rack-amf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,218 @@
1
+ require 'amf/pure/io_helpers'
2
+
3
+ module AMF
4
+ module Pure
5
+ # AMF0 implementation of serializer
6
+ class Serializer
7
+ def initialize
8
+ @ref_cache = SerializerCache.new
9
+ end
10
+
11
+ def version
12
+ 0
13
+ end
14
+
15
+ def serialize obj, stream = ""
16
+ if @ref_cache[obj] != nil
17
+ # Write reference header
18
+ end
19
+ end
20
+ end
21
+
22
+ # AMF3 implementation of serializer
23
+ class AMF3Serializer
24
+ attr_reader :string_cache
25
+
26
+ def initialize
27
+ @string_cache = SerializerCache.new
28
+ @object_cache = SerializerCache.new
29
+ end
30
+
31
+ def version
32
+ 3
33
+ end
34
+
35
+ def serialize obj, stream = ""
36
+ if obj.respond_to?(:to_amf)
37
+ stream << obj.to_amf(self)
38
+ elsif obj.is_a?(NilClass)
39
+ write_null stream
40
+ elsif obj.is_a?(TrueClass)
41
+ write_true stream
42
+ elsif obj.is_a?(FalseClass)
43
+ write_false stream
44
+ elsif obj.is_a?(Float)
45
+ write_float obj, stream
46
+ elsif obj.is_a?(Integer)
47
+ write_integer obj, stream
48
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
49
+ write_string obj.to_s, stream
50
+ elsif obj.is_a?(Time)
51
+ write_date obj, stream
52
+ elsif obj.is_a?(Array)
53
+ write_array obj, stream
54
+ elsif obj.is_a?(Hash) || obj.is_a?(Object)
55
+ write_object obj, stream
56
+ end
57
+ stream
58
+ end
59
+
60
+ def write_reference index, stream
61
+ header = index << 1 # shift value left to leave a low bit of 0
62
+ stream << pack_integer(header)
63
+ end
64
+
65
+ def write_null stream
66
+ stream << AMF3_NULL_MARKER
67
+ end
68
+
69
+ def write_true stream
70
+ stream << AMF3_TRUE_MARKER
71
+ end
72
+
73
+ def write_false stream
74
+ stream << AMF3_FALSE_MARKER
75
+ end
76
+
77
+ def write_integer int, stream
78
+ if int < MIN_INTEGER || int > MAX_INTEGER # Check valid range for 29 bits
79
+ write_float int.to_f, stream
80
+ else
81
+ stream << AMF3_INTEGER_MARKER
82
+ stream << pack_integer(int)
83
+ end
84
+ end
85
+
86
+ def write_float float, stream
87
+ stream << AMF3_DOUBLE_MARKER
88
+ stream << pack_double(float)
89
+ end
90
+
91
+ def write_string str, stream
92
+ stream << AMF3_STRING_MARKER
93
+ write_utf8_vr str, stream
94
+ end
95
+
96
+ def write_date date, stream
97
+ stream << AMF3_DATE_MARKER
98
+ if @object_cache[date] != nil
99
+ write_reference @object_cache[date], stream
100
+ else
101
+ # Cache date
102
+ @object_cache.add_obj date
103
+
104
+ # Build AMF string
105
+ date.utc unless date.utc?
106
+ seconds = (date.to_f * 1000).to_i
107
+ stream << pack_integer(AMF3_NULL_MARKER)
108
+ stream << pack_double(seconds)
109
+ end
110
+ end
111
+
112
+ def write_array array, stream
113
+ stream << AMF3_ARRAY_MARKER
114
+ if @object_cache[array] != nil
115
+ write_reference @object_cache[array], stream
116
+ else
117
+ # Cache array
118
+ @object_cache.add_obj array
119
+
120
+ # Build AMF string
121
+ header = array.length << 1 # make room for a low bit of 1
122
+ header = header | 1 # set the low bit to 1
123
+ stream << pack_integer(header)
124
+ stream << CLOSE_DYNAMIC_ARRAY
125
+ array.each do |elem|
126
+ serialize elem, stream
127
+ end
128
+ end
129
+ end
130
+
131
+ def write_object obj, stream
132
+ stream << AMF3_OBJECT_MARKER
133
+ if @object_cache[obj] != nil
134
+ write_reference @object_cache[obj], stream
135
+ else
136
+ # Cache object
137
+ @object_cache.add_obj obj
138
+
139
+ class_name = ClassMapper.get_as_class_name obj
140
+
141
+ # Any object that has a class name isn't dynamic
142
+ unless class_name
143
+ stream << DYNAMIC_OBJECT
144
+ end
145
+
146
+ # Write class name/anonymous
147
+ if class_name
148
+ write_utf8_vr class_name, stream
149
+ else
150
+ stream << ANONYMOUS_OBJECT
151
+ end
152
+
153
+ # Write out properties
154
+ props = ClassMapper.props_for_serialization obj
155
+ props.sort.each do |key, val| # Sort props until Ruby 1.9 becomes common
156
+ write_utf8_vr key.to_s, stream
157
+ serialize val, stream
158
+ end
159
+
160
+ # Write close
161
+ stream << CLOSE_DYNAMIC_OBJECT
162
+ end
163
+ end
164
+
165
+ private
166
+ include AMF::Pure::WriteIOHelpers
167
+
168
+ def write_utf8_vr str, stream
169
+ if str == ''
170
+ stream << EMPTY_STRING
171
+ elsif @string_cache[str] != nil
172
+ write_reference @string_cache[str], stream
173
+ else
174
+ # Cache string
175
+ @string_cache.add_obj str
176
+
177
+ # Build AMF string
178
+ header = str.length << 1 # make room for a low bit of 1
179
+ header = header | 1 # set the low bit to 1
180
+ stream << pack_integer(header)
181
+ stream << str
182
+ end
183
+ end
184
+ end
185
+
186
+ class SerializerCache #:nodoc:
187
+ def initialize
188
+ @cache_index = 0
189
+ @store = {}
190
+ end
191
+
192
+ def [] obj
193
+ @store[object_key(obj)]
194
+ end
195
+
196
+ def []= obj, value
197
+ @store[object_key(obj)] = value
198
+ end
199
+
200
+ def add_obj obj
201
+ key = object_key obj
202
+ if @store[key].nil?
203
+ @store[key] = @cache_index
204
+ @cache_index += 1
205
+ end
206
+ end
207
+
208
+ private
209
+ def object_key obj
210
+ if obj.is_a?(String)
211
+ obj
212
+ else
213
+ obj.object_id
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
data/lib/amf/pure.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'amf/constants'
2
+ require 'amf/pure/deserializer'
3
+ require 'amf/pure/serializer'
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,9 @@
1
+ module AMF
2
+ module Values #:nodoc:
3
+ class ArrayCollection
4
+ def externalized_data=(data)
5
+ @data = data
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,133 @@
1
+ module AMF
2
+ module Values #: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 :clientId
7
+ attr_accessor :destination
8
+ attr_accessor :messageId
9
+ attr_accessor :timestamp
10
+ attr_accessor :timeToLive
11
+ attr_accessor :headers
12
+ attr_accessor :body
13
+
14
+ def rand_uuid
15
+ [8,4,4,4,12].map {|n| rand_hex_3(n)}.join('-').to_s
16
+ end
17
+
18
+ private
19
+ def rand_hex_3(l)
20
+ "%0#{l}x" % rand(1 << l*4)
21
+ end
22
+ end
23
+
24
+ # Maps to <tt>flex.messaging.messages.RemotingMessage</tt>
25
+ class RemotingMessage < AbstractMessage
26
+ # The name of the service to be called including package name
27
+ attr_accessor :source
28
+
29
+ # The name of the method to be called
30
+ attr_accessor :operation
31
+
32
+ # The arguments to call the method with
33
+ attr_accessor :parameters
34
+
35
+ def initialize
36
+ @clientId = rand_uuid
37
+ @destination = nil
38
+ @messageId = rand_uuid
39
+ @timestamp = Time.new.to_i*100
40
+ @timeToLive = 0
41
+ @headers = {}
42
+ @body = nil
43
+ end
44
+ end
45
+
46
+ # Maps to <tt>flex.messaging.messages.AsyncMessage</tt>
47
+ class AsyncMessage < AbstractMessage
48
+ attr_accessor :correlationId
49
+ end
50
+
51
+ # Maps to <tt>flex.messaging.messages.CommandMessage</tt>
52
+ class CommandMessage < AsyncMessage
53
+ SUBSCRIBE_OPERATION = 0
54
+ UNSUSBSCRIBE_OPERATION = 1
55
+ POLL_OPERATION = 2
56
+ CLIENT_SYNC_OPERATION = 4
57
+ CLIENT_PING_OPERATION = 5
58
+ CLUSTER_REQUEST_OPERATION = 7
59
+ LOGIN_OPERATION = 8
60
+ LOGOUT_OPERATION = 9
61
+ SESSION_INVALIDATE_OPERATION = 10
62
+ MULTI_SUBSCRIBE_OPERATION = 11
63
+ DISCONNECT_OPERATION = 12
64
+ UNKNOWN_OPERATION = 10000
65
+
66
+ attr_accessor :operation
67
+
68
+ def initialize
69
+ @operation = UNKNOWN_OPERATION
70
+ end
71
+ end
72
+
73
+ # Maps to <tt>flex.messaging.messages.AcknowledgeMessage</tt>
74
+ class AcknowledgeMessage < AsyncMessage
75
+ def initialize message=nil
76
+ @clientId = rand_uuid
77
+ @destination = nil
78
+ @messageId = rand_uuid
79
+ @timestamp = Time.new.to_i*100
80
+ @timeToLive = 0
81
+ @headers = {}
82
+ @body = nil
83
+
84
+ if message.is_a?(AbstractMessage)
85
+ @correlationId = message.messageId
86
+ end
87
+ end
88
+ end
89
+
90
+ # Maps to <tt>flex.messaging.messages.ErrorMessage</tt> in AMF3 mode
91
+ class ErrorMessage < AcknowledgeMessage
92
+ # Extended data that will facilitate custom error processing on the client
93
+ attr_accessor :extendedData
94
+
95
+ # The fault code for the error, which defaults to the class name of the
96
+ # causing exception
97
+ attr_accessor :faultCode
98
+
99
+ # Detailed description of what caused the error
100
+ attr_accessor :faultDetail
101
+
102
+ # A simple description of the error
103
+ attr_accessor :faultString
104
+
105
+ # Optional "root cause" of the error
106
+ attr_accessor :rootCause
107
+
108
+ def initialize message, exception
109
+ super message
110
+
111
+ @e = exception
112
+ @faultCode = @e.class.name
113
+ @faultDetail = @e.backtrace.join("\n")
114
+ @faultString = @e.message
115
+ end
116
+
117
+ def to_amf serializer
118
+ stream = ""
119
+ if serializer.version == 0
120
+ data = {
121
+ :faultCode => @faultCode,
122
+ :faultDetail => @faultDetail,
123
+ :faultString => @faultString
124
+ }
125
+ serializer.write_hash(data, stream)
126
+ else
127
+ serializer.write_object(self, stream)
128
+ end
129
+ stream
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,13 @@
1
+ module AMF
2
+ module Values #:nodoc:
3
+ # Hash-like object that can store a type string. Used to preserve type information
4
+ # for unmapped objects after deserialization.
5
+ class TypedHash < Hash
6
+ attr_reader :type
7
+
8
+ def initialize type
9
+ @type = type
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module AMF
2
+ # AMF version
3
+ VERSION = '0.0.1'
4
+ VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ VARIANT_BINARY = false
9
+ end
data/lib/amf.rb ADDED
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+ $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/amf/"
3
+
4
+ require 'rubygems'
5
+ require 'amf/version'
6
+ require 'amf/common'
7
+
8
+ module AMF
9
+ begin
10
+ raise LoadError, 'C extensions not implemented'
11
+ rescue LoadError
12
+ require 'amf/pure'
13
+ end
14
+ require 'amf/class_mapping'
15
+
16
+ ClassMapper = AMF::ClassMapping.new
17
+ end
@@ -0,0 +1,32 @@
1
+ require 'rack/amf/request'
2
+ require 'rack/amf/response'
3
+
4
+ module Rack::AMF
5
+ class Application
6
+ def initialize app, mode
7
+ @app = app
8
+ @mode = mode
9
+ end
10
+
11
+ def call env
12
+ if env['CONTENT_TYPE'] != APPLICATION_AMF
13
+ return [200, {"Content-Type" => "text/plain"}, ["Hello From Rack::AMF"]]
14
+ end
15
+
16
+ # Wrap request and response
17
+ env['amf.request'] = Request.new(env)
18
+ env['amf.response'] = Response.new(env['amf.request'])
19
+
20
+ # Handle request
21
+ if @mode == :pass_through
22
+ @app.call env
23
+ elsif @mode == :internal
24
+ # Have the service manager handle it
25
+ Services.handle(env)
26
+ end
27
+
28
+ response = env['amf.response'].to_s
29
+ [200, {"Content-Type" => APPLICATION_AMF, 'Content-Length' => response.length.to_s}, [response]]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ module Rack::AMF
2
+ class Request
3
+ attr_reader :raw_request
4
+
5
+ def initialize env
6
+ env['rack.input'].rewind
7
+ @raw_request = ::AMF::Request.new.populate_from_stream(env['rack.input'].read)
8
+ end
9
+
10
+ # Returns all messages in the request
11
+ def messages
12
+ @raw_request.messages
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ module Rack::AMF
2
+ class Response
3
+ attr_reader :raw_response
4
+
5
+ def initialize request
6
+ @request = request
7
+ @raw_response = ::AMF::Response.new
8
+ end
9
+
10
+ # Builds response, iterating over each method call and using the return value
11
+ # as the method call's return value
12
+ def each_method_call &block
13
+ @request.messages.each do |m|
14
+ target_uri = m.response_uri
15
+
16
+ rd = m.data
17
+ if rd.is_a?(::AMF::Values::CommandMessage)
18
+ if rd.operation == ::AMF::Values::CommandMessage::CLIENT_PING_OPERATION
19
+ data = ::AMF::Values::AcknowledgeMessage.new(rd)
20
+ else
21
+ data == ::AMF::Values::ErrorMessage.new(Exception.new("CommandMessage #{rd.operation} not implemented"), rd)
22
+ end
23
+ elsif rd.is_a?(::AMF::Values::RemotingMessage)
24
+ am = ::AMF::Values::AcknowledgeMessage.new(rd)
25
+ body = dispatch_call(rd.source+'.'+rd.operation, rd.body, rd, block)
26
+ if body.is_a?(::AMF::Values::ErrorMessage)
27
+ data = body
28
+ else
29
+ am.body = body
30
+ data = am
31
+ end
32
+ else
33
+ data = dispatch_call(m.target_uri, rd, m, block)
34
+ end
35
+
36
+ target_uri += data.is_a?(::AMF::Values::ErrorMessage) ? '/onStatus' : '/onResult'
37
+ @raw_response.messages << ::AMF::Message.new(target_uri, '', data)
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ raw_response.serialize
43
+ end
44
+
45
+ private
46
+ def dispatch_call method, args, source_message, handler
47
+ begin
48
+ handler.call(method, args)
49
+ rescue Exception => e
50
+ ::AMF::Values::ErrorMessage.new(source_message, e)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module Rack::AMF
2
+ class ServiceManager
3
+ def initialize
4
+ @services = {}
5
+ end
6
+
7
+ def register path, service
8
+ @services ||= {}
9
+ @services[path] = service
10
+ end
11
+
12
+ def handle env
13
+ env['amf.response'].each_method_call do |method, args|
14
+ handle_method method, args
15
+ end
16
+ end
17
+
18
+ private
19
+ def handle_method method, args
20
+ path = method.split('.')
21
+ method_name = path.pop
22
+ path = path.join('.')
23
+
24
+ if @services[path]
25
+ if @services[path].respond_to?(method_name)
26
+ @services[path].send(method_name, *args)
27
+ else
28
+ raise "Service #{path} does not respond to #{method_name}"
29
+ end
30
+ else
31
+ raise "Service #{path} does not exist"
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/rack/amf.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'rack'
2
+ require 'amf'
3
+
4
+ require 'rack/amf/application'
5
+ require 'rack/amf/service_manager'
6
+ require 'rack/amf/request'
7
+ require 'rack/amf/response'
8
+
9
+ module Rack::AMF
10
+ APPLICATION_AMF = 'application/x-amf'.freeze
11
+
12
+ Services = Rack::AMF::ServiceManager.new
13
+
14
+ def self.new app, mode=:internal
15
+ Rack::AMF::Application.new(app, mode)
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ describe AMF::ClassMapping::MappingSet do
4
+ before :each do
5
+ @config = AMF::ClassMapping::MappingSet.new
6
+ end
7
+
8
+ it "should retrieve AS mapping for ruby class" do
9
+ @config.map :as => 'ASTest', :ruby => 'RubyTest'
10
+ @config.get_as_class_name('RubyTest').should == 'ASTest'
11
+ @config.get_as_class_name('BadClass').should be_nil
12
+ end
13
+
14
+ it "should retrive ruby class name mapping for AS class" do
15
+ @config.map :as => 'ASTest', :ruby => 'RubyTest'
16
+ @config.get_ruby_class_name('ASTest').should == 'RubyTest'
17
+ @config.get_ruby_class_name('BadClass').should be_nil
18
+ end
19
+
20
+ it "should map special classes by default" do
21
+ SPECIAL_CLASSES = [
22
+ 'flex.messaging.messages.AcknowledgeMessage',
23
+ 'flex.messaging.messages.ErrorMessage',
24
+ 'flex.messaging.messages.CommandMessage',
25
+ 'flex.messaging.messages.ErrorMessage',
26
+ 'flex.messaging.messages.RemotingMessage',
27
+ 'flex.messaging.io.ArrayCollection'
28
+ ]
29
+
30
+ SPECIAL_CLASSES.each do |as_class|
31
+ @config.get_ruby_class_name(as_class).should_not be_nil
32
+ end
33
+ end
34
+ end