amfora 0.0.1
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/.document +5 -0
- data/.gitignore +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +16 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/amfora.gemspec +66 -0
- data/lib/amf.rb +70 -0
- data/lib/amf/active_record.rb +60 -0
- data/lib/amf/class_mapping.rb +137 -0
- data/lib/amf/constants.rb +47 -0
- data/lib/amf/messages.rb +145 -0
- data/lib/amf/pure.rb +14 -0
- data/lib/amf/pure/deserializer.rb +362 -0
- data/lib/amf/pure/io_helpers.rb +94 -0
- data/lib/amf/pure/remoting.rb +119 -0
- data/lib/amf/pure/serializer.rb +230 -0
- data/lib/amf/version.rb +9 -0
- data/lib/amfora.rb +2 -0
- data/lib/rack/amf.rb +12 -0
- data/lib/rack/amf/application.rb +54 -0
- data/spec/amfora_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +90 -0
data/lib/amf/messages.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
module AMF
|
2
|
+
module Messages #:nodoc:
|
3
|
+
# Base class for all special AS3 response messages. Maps to
|
4
|
+
# <tt>flex.messaging.messages.AbstractMessage</tt>
|
5
|
+
class AbstractMessage
|
6
|
+
attr_accessor :client_id
|
7
|
+
attr_accessor :destination
|
8
|
+
attr_accessor :message_id
|
9
|
+
attr_accessor :timestamp
|
10
|
+
attr_accessor :time_to_live
|
11
|
+
attr_accessor :headers
|
12
|
+
attr_accessor :body
|
13
|
+
|
14
|
+
def to_amf(options = {})
|
15
|
+
options[:amf_version] ||= 3
|
16
|
+
options[:serializable_names] = (self.public_methods - Object.new.public_methods).delete_if { |elm| elm =~ /to_amf|body|.*=/ }
|
17
|
+
|
18
|
+
AMF.serialize(self, options) do |s|
|
19
|
+
if body
|
20
|
+
s.write_utf8_vr("body")
|
21
|
+
s.stream << body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
def rand_uuid
|
28
|
+
[8,4,4,4,12].map {|n| rand_hex_3(n)}.join('-').to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def rand_hex_3(l)
|
32
|
+
"%0#{l}x" % rand(1 << l*4)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Maps to <tt>flex.messaging.messages.RemotingMessage</tt>
|
37
|
+
class RemotingMessage < AbstractMessage
|
38
|
+
# The name of the service to be called including package name
|
39
|
+
attr_accessor :source
|
40
|
+
|
41
|
+
# The name of the method to be called
|
42
|
+
attr_accessor :operation
|
43
|
+
|
44
|
+
# The arguments to call the method with
|
45
|
+
attr_accessor :parameters
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@client_id = rand_uuid
|
49
|
+
@destination = nil
|
50
|
+
@message_id = rand_uuid
|
51
|
+
@timestamp = Time.new.to_i*100
|
52
|
+
@time_to_live = 0
|
53
|
+
@headers = {}
|
54
|
+
@body = nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Maps to <tt>flex.messaging.messages.AsyncMessage</tt>
|
59
|
+
class AsyncMessage < AbstractMessage
|
60
|
+
attr_accessor :correlation_id
|
61
|
+
end
|
62
|
+
|
63
|
+
# Maps to <tt>flex.messaging.messages.CommandMessage</tt>
|
64
|
+
class CommandMessage < AsyncMessage
|
65
|
+
SUBSCRIBE_OPERATION = 0
|
66
|
+
UNSUSBSCRIBE_OPERATION = 1
|
67
|
+
POLL_OPERATION = 2
|
68
|
+
CLIENT_SYNC_OPERATION = 4
|
69
|
+
CLIENT_PING_OPERATION = 5
|
70
|
+
CLUSTER_REQUEST_OPERATION = 7
|
71
|
+
LOGIN_OPERATION = 8
|
72
|
+
LOGOUT_OPERATION = 9
|
73
|
+
SESSION_INVALIDATE_OPERATION = 10
|
74
|
+
MULTI_SUBSCRIBE_OPERATION = 11
|
75
|
+
DISCONNECT_OPERATION = 12
|
76
|
+
UNKNOWN_OPERATION = 10000
|
77
|
+
|
78
|
+
attr_accessor :operation
|
79
|
+
|
80
|
+
def initialize
|
81
|
+
@operation = UNKNOWN_OPERATION
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Maps to <tt>flex.messaging.messages.AcknowledgeMessage</tt>
|
86
|
+
class AcknowledgeMessage < AsyncMessage
|
87
|
+
def initialize(message = nil)
|
88
|
+
@client_id = rand_uuid
|
89
|
+
@destination = nil
|
90
|
+
@message_id = rand_uuid
|
91
|
+
@timestamp = Time.new.to_i*100
|
92
|
+
@time_to_live = 0
|
93
|
+
@headers = {}
|
94
|
+
@body = nil
|
95
|
+
|
96
|
+
if message.is_a?(AbstractMessage)
|
97
|
+
@correlation_id = message.message_id
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Maps to <tt>flex.messaging.messages.ErrorMessage</tt> in AMF3 mode
|
103
|
+
class ErrorMessage < AcknowledgeMessage
|
104
|
+
# Extended data that will facilitate custom error processing on the client
|
105
|
+
attr_accessor :extended_data
|
106
|
+
|
107
|
+
# The fault code for the error, which defaults to the class name of the
|
108
|
+
# causing exception
|
109
|
+
attr_accessor :fault_code
|
110
|
+
|
111
|
+
# Detailed description of what caused the error
|
112
|
+
attr_accessor :fault_detail
|
113
|
+
|
114
|
+
# A simple description of the error
|
115
|
+
attr_accessor :fault_string
|
116
|
+
|
117
|
+
# Optional "root cause" of the error
|
118
|
+
attr_accessor :root_cause
|
119
|
+
|
120
|
+
def initialize message, exception
|
121
|
+
super message
|
122
|
+
|
123
|
+
@e = exception
|
124
|
+
@fault_code = @e.class.name
|
125
|
+
@fault_detail = @e.backtrace.join("\n")
|
126
|
+
@fault_string = @e.message
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_amf serializer
|
130
|
+
stream = ""
|
131
|
+
# if serializer.version == 0
|
132
|
+
# data = {
|
133
|
+
# :faultCode => @faultCode,
|
134
|
+
# :faultDetail => @faultDetail,
|
135
|
+
# :faultString => @faultString
|
136
|
+
# }
|
137
|
+
# serializer.write_hash(data, stream)
|
138
|
+
# else
|
139
|
+
# serializer.write_object(self, stream)
|
140
|
+
# end
|
141
|
+
stream
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/amf/pure.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'amf/constants'
|
2
|
+
require 'amf/pure/serializer'
|
3
|
+
require 'amf/pure/deserializer'
|
4
|
+
require 'amf/pure/remoting'
|
5
|
+
|
6
|
+
module AMF
|
7
|
+
# This module holds all the modules/classes that implement AMF's
|
8
|
+
# functionality in pure ruby.
|
9
|
+
module Pure
|
10
|
+
$DEBUG and warn "Using pure library for AMF."
|
11
|
+
end
|
12
|
+
|
13
|
+
include AMF::Pure
|
14
|
+
end
|
@@ -0,0 +1,362 @@
|
|
1
|
+
require 'amf/pure/io_helpers'
|
2
|
+
|
3
|
+
module AMF
|
4
|
+
module Pure
|
5
|
+
# Pure ruby deserializer
|
6
|
+
#--
|
7
|
+
# AMF0 deserializer, it switches over to AMF3 when it sees the switch flag
|
8
|
+
class AMF0Deserializer
|
9
|
+
def initialize
|
10
|
+
@ref_cache = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(source, type=nil)
|
14
|
+
source = StringIO.new(source) unless StringIO === source
|
15
|
+
type = read_int8(source) unless type
|
16
|
+
case type
|
17
|
+
when AMF0_NUMBER_MARKER
|
18
|
+
read_number(source)
|
19
|
+
when AMF0_BOOLEAN_MARKER
|
20
|
+
read_boolean(source)
|
21
|
+
when AMF0_STRING_MARKER
|
22
|
+
read_string(source)
|
23
|
+
when AMF0_OBJECT_MARKER
|
24
|
+
read_object(source)
|
25
|
+
when AMF0_NULL_MARKER
|
26
|
+
nil
|
27
|
+
when AMF0_UNDEFINED_MARKER
|
28
|
+
nil
|
29
|
+
when AMF0_REFERENCE_MARKER
|
30
|
+
read_reference(source)
|
31
|
+
when AMF0_HASH_MARKER
|
32
|
+
read_hash(source)
|
33
|
+
when AMF0_STRICT_ARRAY_MARKER
|
34
|
+
read_array(source)
|
35
|
+
when AMF0_DATE_MARKER
|
36
|
+
read_date(source)
|
37
|
+
when AMF0_LONG_STRING_MARKER
|
38
|
+
read_string(source, true)
|
39
|
+
when AMF0_UNSUPPORTED_MARKER
|
40
|
+
nil
|
41
|
+
when AMF0_XML_MARKER
|
42
|
+
#read_xml source
|
43
|
+
when AMF0_TYPED_OBJECT_MARKER
|
44
|
+
read_typed_object(source)
|
45
|
+
when AMF0_AMF3_MARKER
|
46
|
+
AMF3Deserializer.new.deserialize(source)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
include AMF::Pure::ReadIOHelpers
|
52
|
+
|
53
|
+
def read_number(source)
|
54
|
+
res = read_double(source)
|
55
|
+
res.is_a?(Float)&&res.nan? ? nil : res # check for NaN and convert them to nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_boolean(source)
|
59
|
+
read_int8(source) != 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def read_string(source, long = false)
|
63
|
+
len = long ? read_word32_network(source) : read_word16_network(source)
|
64
|
+
source.read(len)
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_object(source, add_to_ref_cache=true)
|
68
|
+
obj = {}
|
69
|
+
@ref_cache << obj if add_to_ref_cache
|
70
|
+
while true
|
71
|
+
key = read_string(source).underscore
|
72
|
+
type = read_int8(source)
|
73
|
+
break if type == AMF0_OBJECT_END_MARKER
|
74
|
+
obj[key.to_sym] = deserialize(source, type)
|
75
|
+
end
|
76
|
+
obj
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_reference(source)
|
80
|
+
index = read_word16_network(source)
|
81
|
+
@ref_cache[index]
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_hash(source)
|
85
|
+
len = read_word32_network(source) # Read and ignore length
|
86
|
+
|
87
|
+
# Read first pair
|
88
|
+
key = read_string(source).underscore
|
89
|
+
|
90
|
+
type = read_int8(source)
|
91
|
+
return [] if type == AMF0_OBJECT_END_MARKER
|
92
|
+
|
93
|
+
# We need to figure out whether this is a real hash, or whether some stupid serializer gave up
|
94
|
+
if key.to_i.to_s == key
|
95
|
+
# Array
|
96
|
+
obj = []
|
97
|
+
@ref_cache << obj
|
98
|
+
|
99
|
+
obj[key.to_i] = deserialize(source, type)
|
100
|
+
while true
|
101
|
+
key = read_string(source).underscore
|
102
|
+
type = read_int8(source)
|
103
|
+
break if type == AMF0_OBJECT_END_MARKER
|
104
|
+
obj[key.to_i] = deserialize(source, type)
|
105
|
+
end
|
106
|
+
else
|
107
|
+
# Hash
|
108
|
+
obj = {}
|
109
|
+
@ref_cache << obj
|
110
|
+
|
111
|
+
obj[key.to_sym] = deserialize(source, type)
|
112
|
+
while true
|
113
|
+
key = read_string(source).underscore
|
114
|
+
type = read_int8(source)
|
115
|
+
break if type == AMF0_OBJECT_END_MARKER
|
116
|
+
obj[key.to_sym] = deserialize(source, type)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
obj
|
120
|
+
end
|
121
|
+
|
122
|
+
def read_array(source)
|
123
|
+
len = read_word32_network(source)
|
124
|
+
array = []
|
125
|
+
@ref_cache << array
|
126
|
+
|
127
|
+
0.upto(len - 1) do
|
128
|
+
array << deserialize(source)
|
129
|
+
end
|
130
|
+
array
|
131
|
+
end
|
132
|
+
|
133
|
+
def read_date(source)
|
134
|
+
seconds = read_double(source).to_f/1000
|
135
|
+
time = Time.at(seconds)
|
136
|
+
tz = read_word16_network(source) # Unused
|
137
|
+
time
|
138
|
+
end
|
139
|
+
|
140
|
+
def read_typed_object(source)
|
141
|
+
# Create object to add to ref cache
|
142
|
+
class_name = read_string(source)
|
143
|
+
obj = ClassMapper.get_ruby_obj(class_name)
|
144
|
+
@ref_cache << obj
|
145
|
+
|
146
|
+
# Read object props
|
147
|
+
props = read_object(source, false)
|
148
|
+
|
149
|
+
# Populate object
|
150
|
+
ClassMapper.populate_ruby_obj(obj, props, {})
|
151
|
+
return obj
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# AMF3 implementation of deserializer, loaded automatically by the AMF0
|
156
|
+
# deserializer when needed
|
157
|
+
class AMF3Deserializer
|
158
|
+
def initialize
|
159
|
+
@string_cache = []
|
160
|
+
@object_cache = []
|
161
|
+
@trait_cache = []
|
162
|
+
end
|
163
|
+
|
164
|
+
def deserialize(source, type=nil)
|
165
|
+
source = StringIO.new(source) unless StringIO === source
|
166
|
+
type = read_int8 source unless type
|
167
|
+
case type
|
168
|
+
when AMF3_UNDEFINED_MARKER
|
169
|
+
nil
|
170
|
+
when AMF3_NULL_MARKER
|
171
|
+
nil
|
172
|
+
when AMF3_FALSE_MARKER
|
173
|
+
false
|
174
|
+
when AMF3_TRUE_MARKER
|
175
|
+
true
|
176
|
+
when AMF3_INTEGER_MARKER
|
177
|
+
read_integer(source)
|
178
|
+
when AMF3_DOUBLE_MARKER
|
179
|
+
read_number(source)
|
180
|
+
when AMF3_STRING_MARKER
|
181
|
+
read_string(source)
|
182
|
+
when AMF3_XML_DOC_MARKER
|
183
|
+
#read_xml_string
|
184
|
+
when AMF3_DATE_MARKER
|
185
|
+
read_date(source)
|
186
|
+
when AMF3_ARRAY_MARKER
|
187
|
+
read_array(source)
|
188
|
+
when AMF3_OBJECT_MARKER
|
189
|
+
read_object(source)
|
190
|
+
when AMF3_XML_MARKER
|
191
|
+
#read_amf3_xml
|
192
|
+
when AMF3_BYTE_ARRAY_MARKER
|
193
|
+
#read_amf3_byte_array
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
include AMF::Pure::ReadIOHelpers
|
199
|
+
|
200
|
+
def read_integer(source)
|
201
|
+
n = 0
|
202
|
+
b = read_word8(source) || 0
|
203
|
+
result = 0
|
204
|
+
|
205
|
+
while ((b & 0x80) != 0 && n < 3)
|
206
|
+
result = result << 7
|
207
|
+
result = result | (b & 0x7f)
|
208
|
+
b = read_word8(source) || 0
|
209
|
+
n = n + 1
|
210
|
+
end
|
211
|
+
|
212
|
+
if (n < 3)
|
213
|
+
result = result << 7
|
214
|
+
result = result | b
|
215
|
+
else
|
216
|
+
#Use all 8 bits from the 4th byte
|
217
|
+
result = result << 8
|
218
|
+
result = result | b
|
219
|
+
|
220
|
+
#Check if the integer should be negative
|
221
|
+
if (result > MAX_INTEGER)
|
222
|
+
result -= (1 << 29)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
result
|
226
|
+
end
|
227
|
+
|
228
|
+
def read_number(source)
|
229
|
+
res = read_double(source)
|
230
|
+
res.is_a?(Float)&&res.nan? ? nil : res # check for NaN and convert them to nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def read_string(source)
|
234
|
+
type = read_integer(source)
|
235
|
+
is_reference = (type & 0x01) == 0
|
236
|
+
|
237
|
+
if is_reference
|
238
|
+
reference = type >> 1
|
239
|
+
return @string_cache[reference]
|
240
|
+
else
|
241
|
+
length = type >> 1
|
242
|
+
#HACK needed for ['',''] array of empty strings
|
243
|
+
#It may be better to take one more parameter that
|
244
|
+
#would specify whether or not they expect us to return
|
245
|
+
#a string
|
246
|
+
str = "" #if stringRequest
|
247
|
+
if length > 0
|
248
|
+
str = source.read(length)
|
249
|
+
@string_cache << str
|
250
|
+
end
|
251
|
+
return str
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def read_array(source)
|
256
|
+
type = read_integer(source)
|
257
|
+
is_reference = (type & 0x01) == 0
|
258
|
+
|
259
|
+
if is_reference
|
260
|
+
reference = type >> 1
|
261
|
+
return @object_cache[reference]
|
262
|
+
else
|
263
|
+
length = type >> 1
|
264
|
+
property_name = read_string(source).underscore
|
265
|
+
if property_name != ""
|
266
|
+
array = {}
|
267
|
+
@object_cache << array
|
268
|
+
begin
|
269
|
+
while(property_name.length)
|
270
|
+
value = deserialize(source)
|
271
|
+
array[property_name] = value
|
272
|
+
property_name = read_string(source)
|
273
|
+
end
|
274
|
+
rescue Exception => e #end of object exception, because property_name.length will be non existent
|
275
|
+
end
|
276
|
+
0.upto(length - 1) do |i|
|
277
|
+
array["" + i.to_s] = deserialize(source)
|
278
|
+
end
|
279
|
+
else
|
280
|
+
array = []
|
281
|
+
@object_cache << array
|
282
|
+
0.upto(length - 1) do
|
283
|
+
array << deserialize(source)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
array
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def read_object(source)
|
291
|
+
type = read_integer(source)
|
292
|
+
is_reference = (type & 0x01) == 0
|
293
|
+
|
294
|
+
if is_reference
|
295
|
+
reference = type >> 1
|
296
|
+
return @object_cache[reference]
|
297
|
+
else
|
298
|
+
class_type = type >> 1
|
299
|
+
class_is_reference = (class_type & 0x01) == 0
|
300
|
+
|
301
|
+
if class_is_reference
|
302
|
+
reference = class_type >> 1
|
303
|
+
class_definition = @trait_cache[reference]
|
304
|
+
else
|
305
|
+
class_name = read_string(source)
|
306
|
+
externalizable = (class_type & 0x02) != 0
|
307
|
+
dynamic = (class_type & 0x04) != 0
|
308
|
+
attribute_count = class_type >> 3
|
309
|
+
|
310
|
+
class_attributes = []
|
311
|
+
attribute_count.times{class_attributes << read_string(source)} # Read class members
|
312
|
+
|
313
|
+
class_definition = {"class_name" => class_name,
|
314
|
+
"members" => class_attributes,
|
315
|
+
"externalizable" => externalizable,
|
316
|
+
"dynamic" => dynamic}
|
317
|
+
@trait_cache << class_definition
|
318
|
+
end
|
319
|
+
|
320
|
+
obj = ClassMapper.get_ruby_obj(class_definition["class_name"])
|
321
|
+
@object_cache << obj
|
322
|
+
|
323
|
+
if class_definition['externalizable']
|
324
|
+
obj.externalized_data = deserialize(source)
|
325
|
+
else
|
326
|
+
props = {}
|
327
|
+
class_definition['members'].each do |key|
|
328
|
+
value = deserialize(source)
|
329
|
+
props[key.underscore.to_sym] = value
|
330
|
+
end
|
331
|
+
|
332
|
+
dynamic_props = nil
|
333
|
+
if class_definition['dynamic']
|
334
|
+
dynamic_props = {}
|
335
|
+
while (key = read_string(source).underscore) && key.length != 0 do # read next key
|
336
|
+
value = deserialize(source)
|
337
|
+
dynamic_props[key.to_sym] = value
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
ClassMapper.populate_ruby_obj(obj, props, dynamic_props)
|
342
|
+
end
|
343
|
+
obj
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def read_date(source)
|
348
|
+
type = read_integer(source)
|
349
|
+
is_reference = (type & 0x01) == 0
|
350
|
+
if is_reference
|
351
|
+
reference = type >> 1
|
352
|
+
return @object_cache[reference]
|
353
|
+
else
|
354
|
+
seconds = read_double(source).to_f/1000
|
355
|
+
time = Time.at(seconds)
|
356
|
+
@object_cache << time
|
357
|
+
time
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|