meteor-motion 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/.gitignore +21 -0
- data/.repl_history +0 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +166 -0
- data/Rakefile +17 -0
- data/app/app_delegate.rb +11 -0
- data/app/controllers/book_controller.rb +92 -0
- data/app/controllers/book_list_controller.rb +105 -0
- data/app/controllers/connection_controller.rb +83 -0
- data/app/controllers/login_controller.rb +35 -0
- data/lib/meteor-motion.rb +12 -0
- data/lib/meteor-motion/version.rb +3 -0
- data/meteor-motion.gemspec +27 -0
- data/motion/adapters/motion_model.rb +61 -0
- data/motion/client.rb +179 -0
- data/motion/collection.rb +50 -0
- data/motion/collections/default.rb +56 -0
- data/motion/collections/motion_model.rb +52 -0
- data/motion/ddp.rb +161 -0
- data/motion/srp/securerandom.rb +248 -0
- data/motion/srp/srp.rb +250 -0
- data/spec/adapters/motion_model_spec.rb +38 -0
- data/spec/client_spec.rb +104 -0
- data/spec/collection_spec.rb +63 -0
- data/spec/collections/default_spec.rb +46 -0
- data/spec/collections/motion_model_spec.rb +69 -0
- data/spec/ddp_spec.rb +123 -0
- data/spec/server/.meteor/.gitignore +1 -0
- data/spec/server/.meteor/packages +9 -0
- data/spec/server/.meteor/release +1 -0
- data/spec/server/collections/books.js +11 -0
- data/spec/server/server/fixtures.js +28 -0
- data/spec/server/server/publications.js +3 -0
- data/spec/server/smart.json +3 -0
- data/vendor/SocketRocket/NSData+SRB64Additions.h +24 -0
- data/vendor/SocketRocket/NSData+SRB64Additions.m +39 -0
- data/vendor/SocketRocket/SRWebSocket.h +114 -0
- data/vendor/SocketRocket/SRWebSocket.m +1757 -0
- data/vendor/SocketRocket/SocketRocket-Prefix.pch +27 -0
- data/vendor/SocketRocket/SocketRocket.bridgesupport +160 -0
- data/vendor/SocketRocket/base64.c +314 -0
- data/vendor/SocketRocket/base64.h +34 -0
- metadata +190 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
class ConnectionController < UIViewController
|
2
|
+
|
3
|
+
def viewDidLoad
|
4
|
+
super
|
5
|
+
|
6
|
+
self.view.backgroundColor = UIColor.whiteColor
|
7
|
+
@label = UILabel.alloc.initWithFrame(CGRectZero)
|
8
|
+
@label.text = "Connecting..."
|
9
|
+
@label.sizeToFit
|
10
|
+
@label.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2)
|
11
|
+
self.view.addSubview @label
|
12
|
+
|
13
|
+
@meteorClient = MeteorMotion::Client.new
|
14
|
+
@meteorClient.on_error( self.method(:meteor_error) )
|
15
|
+
@meteorClient.connect 'localhost', 3000, self.method(:connected)
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def connected result, details
|
20
|
+
if result
|
21
|
+
@meteorClient.add_collection('books')
|
22
|
+
@meteorClient.subscribe('books')
|
23
|
+
|
24
|
+
form = buildLoginForm
|
25
|
+
|
26
|
+
@controller = LoginController.alloc.initWithForm(form)
|
27
|
+
@controller.meteor = @meteorClient
|
28
|
+
|
29
|
+
self.presentViewController( @controller, animated: true, completion: nil )
|
30
|
+
else
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Meteor basic error handler - alert error to screen
|
36
|
+
#
|
37
|
+
def meteor_error code, reason, details
|
38
|
+
if reason != :unknown
|
39
|
+
alert = UIAlertView.new
|
40
|
+
alert.message = reason.to_s
|
41
|
+
alert.addButtonWithTitle "OK"
|
42
|
+
alert.show
|
43
|
+
else
|
44
|
+
puts "Meteor Error: #{reason}. Details: #{details}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def buildLoginForm
|
50
|
+
form = Formotion::Form.new
|
51
|
+
|
52
|
+
form.build_section do |section|
|
53
|
+
section.title = "Credentials"
|
54
|
+
|
55
|
+
section.build_row do |row|
|
56
|
+
row.title = "Username"
|
57
|
+
row.key = :user
|
58
|
+
row.type = :string
|
59
|
+
row.auto_correction = :no
|
60
|
+
row.auto_capitalization = :none
|
61
|
+
row.placeholder = 'Your username'
|
62
|
+
end
|
63
|
+
|
64
|
+
section.build_row do |row|
|
65
|
+
row.title = "Password"
|
66
|
+
row.key = :pass
|
67
|
+
row.type = :string
|
68
|
+
row.secure = true
|
69
|
+
row.auto_correction = :no
|
70
|
+
row.auto_capitalization = :none
|
71
|
+
row.placeholder = 'Your password'
|
72
|
+
end
|
73
|
+
|
74
|
+
section.build_row do |row|
|
75
|
+
row.title = 'Submit'
|
76
|
+
row.type = :submit
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
return form
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class LoginController < Formotion::FormController
|
2
|
+
attr_accessor :meteor
|
3
|
+
|
4
|
+
def viewDidLoad
|
5
|
+
super
|
6
|
+
|
7
|
+
self.view.backgroundColor = UIColor.whiteColor
|
8
|
+
self.title = "Login"
|
9
|
+
|
10
|
+
self.form.on_submit do |form|
|
11
|
+
data = form.render
|
12
|
+
self.view.endEditing(true)
|
13
|
+
|
14
|
+
@meteor.login_with_username( data[:user], data[:pass], self.method(:login_handler) )
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def login_handler action, details
|
20
|
+
if action == :success
|
21
|
+
controller = BookListController.alloc.initWithNibName(nil, bundle: nil)
|
22
|
+
controller.meteor = @meteor
|
23
|
+
navigationController = UINavigationController.alloc.initWithRootViewController(controller)
|
24
|
+
|
25
|
+
self.presentViewController(navigationController , animated: true, completion: nil)
|
26
|
+
|
27
|
+
elsif action == :error
|
28
|
+
alert = UIAlertView.new
|
29
|
+
alert.message = "Error: #{details['reason'].to_s}"
|
30
|
+
alert.addButtonWithTitle "OK"
|
31
|
+
alert.show
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "meteor-motion/version"
|
2
|
+
BW.require 'motion/**/*.rb'
|
3
|
+
|
4
|
+
unless defined?(Motion::Project::Config)
|
5
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
6
|
+
end
|
7
|
+
|
8
|
+
Motion::Project::App.setup do |app|
|
9
|
+
app.vendor_project(File.expand_path(File.join(File.dirname(__FILE__), '../vendor/SocketRocket')), :static, cflags: "-fobjc-arc")
|
10
|
+
app.libs += ['/usr/lib/libicucore.dylib']
|
11
|
+
app.frameworks += ['CFNetwork', 'Security', 'Foundation']
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'meteor-motion/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "meteor-motion"
|
8
|
+
spec.version = MeteorMotion::VERSION
|
9
|
+
spec.authors = ["Miguel Tavares"]
|
10
|
+
spec.email = ["mtavares.azrael@gmail.com"]
|
11
|
+
spec.description = %q{Write a gem description}
|
12
|
+
spec.summary = %q{Write a gem summary}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'bubble-wrap'
|
22
|
+
spec.add_dependency 'rm-digest'
|
23
|
+
spec.add_dependency 'motion_model'
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "motion-redgreen"
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module MeteorMotion
|
2
|
+
module Adapters
|
3
|
+
module MotionModel
|
4
|
+
include ::MotionModel::ArrayModelAdapter
|
5
|
+
|
6
|
+
def self.set_client client
|
7
|
+
@@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
# Sends the data to the server and supers for the record to be saved. If saving on the server fails
|
13
|
+
# it has to be dealt with by the user.
|
14
|
+
def do_insert options = {}
|
15
|
+
if !options[:local]
|
16
|
+
attr_hash = build_attr_hash
|
17
|
+
id = SecureRandom.hex
|
18
|
+
@@client.call("/#{self.class.to_s.downcase}/insert", self.method(:handle_update), [{_id: id}.merge(attr_hash)])
|
19
|
+
end
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def do_update options = {}
|
25
|
+
if !options[:local]
|
26
|
+
id = self.get_attr(:id)
|
27
|
+
@@client.call("/#{self.class.to_s.downcase}/update", self.method(:handle_update), [{_id: id}, attr_hash])
|
28
|
+
end
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def do_delete
|
34
|
+
send_remove
|
35
|
+
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_remove
|
40
|
+
id = self.get_attr(:id)
|
41
|
+
@@client.call("/#{self.class.to_s.downcase}/remove", self.method(:handle_remove), [{_id: id}])
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_attr_hash
|
45
|
+
attributes.reject{|k,v| k == :id}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Stubs to handle inserts, updates and deletes. Can be overrriden by superclass
|
49
|
+
#
|
50
|
+
def handle_insert action, result; end
|
51
|
+
def handle_update action, result; end
|
52
|
+
def handle_remove action, result; end
|
53
|
+
|
54
|
+
# Nasty, nasty hack to work around inheritance issues. TODO: refactor as adapter.
|
55
|
+
def superclass
|
56
|
+
return Object
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/motion/client.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
module MeteorMotion
|
2
|
+
class Client
|
3
|
+
attr_accessor :collections, :subscriptions, :method_callbacks
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@collections = {}
|
7
|
+
@subscriptions = {}
|
8
|
+
@method_callbacks = {}
|
9
|
+
|
10
|
+
@auth_client = nil
|
11
|
+
@auth_key = nil
|
12
|
+
@username = nil
|
13
|
+
@password = nil
|
14
|
+
|
15
|
+
@ddp = MeteorMotion::DDP.new self
|
16
|
+
@error_handler = nil
|
17
|
+
|
18
|
+
MeteorMotion::Adapters::MotionModel.set_client(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def connect hostname='localhost', port=3000, callback=nil
|
23
|
+
@ddp.connect hostname, port
|
24
|
+
if callback
|
25
|
+
@method_callbacks['connect'] = callback
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_connect status
|
30
|
+
if @method_callbacks['connect']
|
31
|
+
obj = { method: @method_callbacks['connect'], action: status == :success, result: nil}
|
32
|
+
self.performSelectorInBackground('background_method_handler:', withObject: obj)
|
33
|
+
@method_callbacks.delete('connect')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_collection klass, name = ""
|
38
|
+
if klass.is_a? String
|
39
|
+
if @collections[klass]
|
40
|
+
raise "A MeteorMotion::Collection named '#{klass}' already exists."
|
41
|
+
end
|
42
|
+
|
43
|
+
@collections[klass] = MeteorMotion::Collections::Default.new klass
|
44
|
+
else
|
45
|
+
coll = MeteorMotion::Collections::MotionModel.new klass, name
|
46
|
+
|
47
|
+
@collections[coll.name] = coll
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def remove_collection name
|
53
|
+
@collections.delete(name)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def subscribe pub_name, params=[]
|
58
|
+
sub_id = @ddp.sub(pub_name, params)
|
59
|
+
@subscriptions[sub_id] = {name: pub_name, ready: false}
|
60
|
+
|
61
|
+
return sub_id
|
62
|
+
end
|
63
|
+
|
64
|
+
def unsubscribe sub_id
|
65
|
+
@ddp.unsub(sub_id)
|
66
|
+
@subscriptions.delete(sub_id)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def call method_name, callback, params=[]
|
71
|
+
method_id = @ddp.call(method_name, params)
|
72
|
+
|
73
|
+
@method_callbacks[method_id] = { method: callback, result: false, updated: false}
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Methods for SRP authentication
|
78
|
+
#
|
79
|
+
def login_with_username username, password, callback
|
80
|
+
@method_callbacks['login'] = callback
|
81
|
+
@auth_client = SRP::Client.new
|
82
|
+
@username = username
|
83
|
+
@password = password
|
84
|
+
|
85
|
+
|
86
|
+
a = @auth_client.start_authentication()
|
87
|
+
id = @ddp.call( 'beginPasswordExchange', { 'A' => a, user: { username: username } } )
|
88
|
+
|
89
|
+
@method_callbacks[id] = { method: self.method(:handle_login_challenge), result: false, updated: false }
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_login_challenge action, result
|
93
|
+
if action == :updated
|
94
|
+
return
|
95
|
+
elsif action == :error
|
96
|
+
obj = { method: @method_callbacks['login'], action: action, result: result }
|
97
|
+
self.performSelectorInBackground('background_method_handler:', withObject: obj)
|
98
|
+
|
99
|
+
@method_callbacks.delete('login')
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
103
|
+
m = @auth_client.process_challenge(result['identity'], @password, result['salt'], result['B'])
|
104
|
+
id = @ddp.call( 'login', {srp: {'M' => m} } )
|
105
|
+
|
106
|
+
@method_callbacks[id] = { method: self.method(:verify_login), result: false, updated: false }
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_login action, result
|
110
|
+
if action == :updated
|
111
|
+
return
|
112
|
+
elsif action == :error
|
113
|
+
obj = { method: @method_callbacks['login'], action: action, result: result }
|
114
|
+
self.performSelectorInBackground('background_method_handler:', withObject: obj)
|
115
|
+
|
116
|
+
@method_callbacks.delete('login')
|
117
|
+
return
|
118
|
+
end
|
119
|
+
|
120
|
+
if @auth_client.verify result['HAMK']
|
121
|
+
obj = { method: @method_callbacks['login'], action: :success, result: nil }
|
122
|
+
else
|
123
|
+
obj = { method: @method_callbacks['login'], action: :error, result: {reason: 'Failed HAMK validation.'} }
|
124
|
+
end
|
125
|
+
|
126
|
+
self.performSelectorInBackground('background_method_handler:', withObject: obj)
|
127
|
+
@method_callbacks.delete('login')
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def on_error method
|
132
|
+
@error_handler = method
|
133
|
+
end
|
134
|
+
|
135
|
+
# Methods required for MeteorMotion::DDP delegation
|
136
|
+
#
|
137
|
+
def handle_method id, action, result
|
138
|
+
callback = @method_callbacks[id]
|
139
|
+
|
140
|
+
if !callback
|
141
|
+
return
|
142
|
+
end
|
143
|
+
|
144
|
+
obj = { method: callback[:method], action: action, result: result}
|
145
|
+
|
146
|
+
self.performSelectorInBackground('background_method_handler:', withObject: obj)
|
147
|
+
|
148
|
+
if callback[action] == :error
|
149
|
+
callback[action] = :result
|
150
|
+
end
|
151
|
+
|
152
|
+
callback[action] = true
|
153
|
+
if callback[:result] && callback[:updated]
|
154
|
+
@method_callbacks.delete(id)
|
155
|
+
else
|
156
|
+
@method_callbacks[id] = callback
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def error code, reason, details
|
161
|
+
if @error_handler
|
162
|
+
obj = { method: @error_handler, code: code, reason: reason, details: details}
|
163
|
+
self.performSelectorInBackground('background_error_handler:', withObject: obj)
|
164
|
+
else
|
165
|
+
#TODO: silent error handling
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
def background_method_handler obj
|
171
|
+
obj[:method].call( obj[:action], obj[:result])
|
172
|
+
end
|
173
|
+
|
174
|
+
def background_error_handler obj
|
175
|
+
obj[:method].call( obj[:code], obj[:reason], obj[:details])
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module MeteorMotion
|
2
|
+
class Collection
|
3
|
+
attr_accessor :name, :observers
|
4
|
+
|
5
|
+
def initialize name
|
6
|
+
@name = name
|
7
|
+
@observers = []
|
8
|
+
end
|
9
|
+
|
10
|
+
# Method hooks - be sure to call super after reimplementing these
|
11
|
+
# Right now these are here more for testing purposes, should be reimplemented as a hook with some testing compromise
|
12
|
+
#
|
13
|
+
def add id, fields
|
14
|
+
notify_observers :added, id
|
15
|
+
end
|
16
|
+
|
17
|
+
def update id, fields, cleared
|
18
|
+
notify_observers :changed, id
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove id
|
22
|
+
notify_observers :removed, id
|
23
|
+
end
|
24
|
+
|
25
|
+
# Observer methods
|
26
|
+
#
|
27
|
+
def add_observer method
|
28
|
+
@observers << method
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_observer method
|
32
|
+
@observers.delete( method )
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def notify_observers action, id
|
37
|
+
obj = {action: action, id: id}
|
38
|
+
self.performSelectorInBackground('background_notify:', withObject: obj)
|
39
|
+
end
|
40
|
+
|
41
|
+
# These callbacks should be done as background tasks to prevent the connection thread from holding in intensive tasks
|
42
|
+
#
|
43
|
+
def background_notify object
|
44
|
+
observers.each do |method_name|
|
45
|
+
#puts "Calling method with action #{object[:action]}"
|
46
|
+
method_name.call( object[:action], object[:id] )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|