hyper-operation 0.5.12 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (277) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.travis.yml +33 -0
  4. data/DOCS-POLICIES.md +93 -0
  5. data/DOCS.md +1 -1
  6. data/Gemfile +5 -1
  7. data/Gemfile.lock +379 -0
  8. data/README.md +10 -9
  9. data/Rakefile +10 -2
  10. data/hyper-operation.gemspec +34 -29
  11. data/lib/hyper-operation.rb +5 -4
  12. data/lib/hyper-operation/boot.rb +1 -1
  13. data/lib/hyper-operation/engine.rb +1 -1
  14. data/lib/hyper-operation/http.rb +309 -0
  15. data/lib/hyper-operation/railway/params_wrapper.rb +1 -0
  16. data/lib/hyper-operation/server_op.rb +83 -18
  17. data/lib/hyper-operation/transport/client_drivers.rb +71 -28
  18. data/lib/hyper-operation/transport/connection.rb +22 -20
  19. data/lib/hyper-operation/transport/hyperloop.rb +1 -1
  20. data/lib/hyper-operation/transport/hyperloop_controller.rb +6 -1
  21. data/lib/hyper-operation/transport/policy.rb +78 -13
  22. data/lib/hyper-operation/version.rb +1 -1
  23. metadata +95 -319
  24. data/CODE_OF_CONDUCT.md +0 -49
  25. data/examples/chat-app/.gitignore +0 -21
  26. data/examples/chat-app/Gemfile +0 -57
  27. data/examples/chat-app/Gemfile.lock +0 -283
  28. data/examples/chat-app/README.md +0 -3
  29. data/examples/chat-app/Rakefile +0 -6
  30. data/examples/chat-app/app/assets/config/manifest.js +0 -3
  31. data/examples/chat-app/app/assets/images/.keep +0 -0
  32. data/examples/chat-app/app/assets/javascripts/application.js +0 -3
  33. data/examples/chat-app/app/assets/javascripts/cable.js +0 -13
  34. data/examples/chat-app/app/assets/javascripts/channels/.keep +0 -0
  35. data/examples/chat-app/app/assets/stylesheets/application.css +0 -15
  36. data/examples/chat-app/app/channels/application_cable/channel.rb +0 -4
  37. data/examples/chat-app/app/channels/application_cable/connection.rb +0 -4
  38. data/examples/chat-app/app/controllers/application_controller.rb +0 -3
  39. data/examples/chat-app/app/controllers/concerns/.keep +0 -0
  40. data/examples/chat-app/app/controllers/home_controller.rb +0 -5
  41. data/examples/chat-app/app/helpers/application_helper.rb +0 -2
  42. data/examples/chat-app/app/hyperloop/components/app.rb +0 -11
  43. data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -13
  44. data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -29
  45. data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
  46. data/examples/chat-app/app/hyperloop/components/messages.rb +0 -9
  47. data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
  48. data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -56
  49. data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -23
  50. data/examples/chat-app/app/models/application_record.rb +0 -3
  51. data/examples/chat-app/app/models/concerns/.keep +0 -0
  52. data/examples/chat-app/app/models/models.rb +0 -2
  53. data/examples/chat-app/app/models/public/.keep +0 -0
  54. data/examples/chat-app/app/models/public/announcement.rb +0 -8
  55. data/examples/chat-app/app/policies/application_policy.rb +0 -5
  56. data/examples/chat-app/app/views/layouts/application.html.erb +0 -51
  57. data/examples/chat-app/bin/bundle +0 -3
  58. data/examples/chat-app/bin/rails +0 -9
  59. data/examples/chat-app/bin/rake +0 -9
  60. data/examples/chat-app/bin/setup +0 -34
  61. data/examples/chat-app/bin/spring +0 -17
  62. data/examples/chat-app/bin/update +0 -29
  63. data/examples/chat-app/config.ru +0 -5
  64. data/examples/chat-app/config/application.rb +0 -13
  65. data/examples/chat-app/config/boot.rb +0 -3
  66. data/examples/chat-app/config/cable.yml +0 -9
  67. data/examples/chat-app/config/database.yml +0 -25
  68. data/examples/chat-app/config/environment.rb +0 -5
  69. data/examples/chat-app/config/environments/development.rb +0 -56
  70. data/examples/chat-app/config/environments/production.rb +0 -86
  71. data/examples/chat-app/config/environments/test.rb +0 -42
  72. data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
  73. data/examples/chat-app/config/initializers/assets.rb +0 -11
  74. data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
  75. data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
  76. data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
  77. data/examples/chat-app/config/initializers/hyperloop.rb +0 -4
  78. data/examples/chat-app/config/initializers/inflections.rb +0 -16
  79. data/examples/chat-app/config/initializers/mime_types.rb +0 -4
  80. data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
  81. data/examples/chat-app/config/initializers/session_store.rb +0 -3
  82. data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
  83. data/examples/chat-app/config/locales/en.yml +0 -23
  84. data/examples/chat-app/config/puma.rb +0 -47
  85. data/examples/chat-app/config/routes.rb +0 -5
  86. data/examples/chat-app/config/secrets.yml +0 -22
  87. data/examples/chat-app/config/spring.rb +0 -6
  88. data/examples/chat-app/db/seeds.rb +0 -7
  89. data/examples/chat-app/lib/assets/.keep +0 -0
  90. data/examples/chat-app/lib/tasks/.keep +0 -0
  91. data/examples/chat-app/log/.keep +0 -0
  92. data/examples/chat-app/public/404.html +0 -67
  93. data/examples/chat-app/public/422.html +0 -67
  94. data/examples/chat-app/public/500.html +0 -66
  95. data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
  96. data/examples/chat-app/public/apple-touch-icon.png +0 -0
  97. data/examples/chat-app/public/favicon.ico +0 -0
  98. data/examples/chat-app/public/robots.txt +0 -5
  99. data/examples/chat-app/test/controllers/.keep +0 -0
  100. data/examples/chat-app/test/fixtures/.keep +0 -0
  101. data/examples/chat-app/test/fixtures/files/.keep +0 -0
  102. data/examples/chat-app/test/helpers/.keep +0 -0
  103. data/examples/chat-app/test/integration/.keep +0 -0
  104. data/examples/chat-app/test/mailers/.keep +0 -0
  105. data/examples/chat-app/test/models/.keep +0 -0
  106. data/examples/chat-app/test/test_helper.rb +0 -10
  107. data/examples/chat-app/tmp/.keep +0 -0
  108. data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
  109. data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
  110. data/examples/five-letter-word-game/.gitignore +0 -21
  111. data/examples/five-letter-word-game/Gemfile +0 -62
  112. data/examples/five-letter-word-game/Gemfile.lock +0 -291
  113. data/examples/five-letter-word-game/README.md +0 -24
  114. data/examples/five-letter-word-game/Rakefile +0 -6
  115. data/examples/five-letter-word-game/app/assets/config/manifest.js +0 -3
  116. data/examples/five-letter-word-game/app/assets/images/.keep +0 -0
  117. data/examples/five-letter-word-game/app/assets/javascripts/application.js +0 -4
  118. data/examples/five-letter-word-game/app/assets/javascripts/cable.js +0 -13
  119. data/examples/five-letter-word-game/app/assets/javascripts/channels/.keep +0 -0
  120. data/examples/five-letter-word-game/app/assets/stylesheets/application.css +0 -15
  121. data/examples/five-letter-word-game/app/channels/application_cable/channel.rb +0 -4
  122. data/examples/five-letter-word-game/app/channels/application_cable/connection.rb +0 -4
  123. data/examples/five-letter-word-game/app/controllers/application_controller.rb +0 -14
  124. data/examples/five-letter-word-game/app/controllers/concerns/.keep +0 -0
  125. data/examples/five-letter-word-game/app/controllers/home_controller.rb +0 -5
  126. data/examples/five-letter-word-game/app/helpers/application_helper.rb +0 -2
  127. data/examples/five-letter-word-game/app/hyperloop/components/app.rb +0 -65
  128. data/examples/five-letter-word-game/app/hyperloop/components/guesses.rb +0 -8
  129. data/examples/five-letter-word-game/app/hyperloop/components/input_word.rb +0 -13
  130. data/examples/five-letter-word-game/app/hyperloop/models/user.rb +0 -27
  131. data/examples/five-letter-word-game/app/hyperloop/operations/ops.rb +0 -115
  132. data/examples/five-letter-word-game/app/hyperloop/stores/store.rb +0 -120
  133. data/examples/five-letter-word-game/app/jobs/application_job.rb +0 -2
  134. data/examples/five-letter-word-game/app/mailers/application_mailer.rb +0 -4
  135. data/examples/five-letter-word-game/app/models/application_record.rb +0 -3
  136. data/examples/five-letter-word-game/app/models/concerns/.keep +0 -0
  137. data/examples/five-letter-word-game/app/policies/hyperloop/application_policy.rb +0 -3
  138. data/examples/five-letter-word-game/app/policies/user_policy.rb +0 -4
  139. data/examples/five-letter-word-game/app/views/layouts/application.html.erb +0 -14
  140. data/examples/five-letter-word-game/app/views/layouts/mailer.html.erb +0 -13
  141. data/examples/five-letter-word-game/app/views/layouts/mailer.text.erb +0 -1
  142. data/examples/five-letter-word-game/bin/bundle +0 -3
  143. data/examples/five-letter-word-game/bin/rails +0 -9
  144. data/examples/five-letter-word-game/bin/rake +0 -9
  145. data/examples/five-letter-word-game/bin/setup +0 -34
  146. data/examples/five-letter-word-game/bin/spring +0 -17
  147. data/examples/five-letter-word-game/bin/update +0 -29
  148. data/examples/five-letter-word-game/config.ru +0 -5
  149. data/examples/five-letter-word-game/config/application.rb +0 -12
  150. data/examples/five-letter-word-game/config/boot.rb +0 -3
  151. data/examples/five-letter-word-game/config/cable.yml +0 -9
  152. data/examples/five-letter-word-game/config/database.yml +0 -46
  153. data/examples/five-letter-word-game/config/environment.rb +0 -5
  154. data/examples/five-letter-word-game/config/environments/development.rb +0 -56
  155. data/examples/five-letter-word-game/config/environments/production.rb +0 -86
  156. data/examples/five-letter-word-game/config/environments/test.rb +0 -42
  157. data/examples/five-letter-word-game/config/initializers/application_controller_renderer.rb +0 -6
  158. data/examples/five-letter-word-game/config/initializers/assets.rb +0 -15
  159. data/examples/five-letter-word-game/config/initializers/backtrace_silencers.rb +0 -7
  160. data/examples/five-letter-word-game/config/initializers/cookies_serializer.rb +0 -5
  161. data/examples/five-letter-word-game/config/initializers/filter_parameter_logging.rb +0 -4
  162. data/examples/five-letter-word-game/config/initializers/hyperloop.rb +0 -19
  163. data/examples/five-letter-word-game/config/initializers/inflections.rb +0 -16
  164. data/examples/five-letter-word-game/config/initializers/mime_types.rb +0 -4
  165. data/examples/five-letter-word-game/config/initializers/new_framework_defaults.rb +0 -24
  166. data/examples/five-letter-word-game/config/initializers/session_store.rb +0 -3
  167. data/examples/five-letter-word-game/config/initializers/wrap_parameters.rb +0 -14
  168. data/examples/five-letter-word-game/config/locales/en.yml +0 -23
  169. data/examples/five-letter-word-game/config/puma.rb +0 -47
  170. data/examples/five-letter-word-game/config/routes.rb +0 -5
  171. data/examples/five-letter-word-game/config/secrets.yml +0 -22
  172. data/examples/five-letter-word-game/config/spring.rb +0 -6
  173. data/examples/five-letter-word-game/db/schema.rb +0 -28
  174. data/examples/five-letter-word-game/db/seeds.rb +0 -7
  175. data/examples/five-letter-word-game/lib/assets/.keep +0 -0
  176. data/examples/five-letter-word-game/lib/tasks/.keep +0 -0
  177. data/examples/five-letter-word-game/log/.keep +0 -0
  178. data/examples/five-letter-word-game/public/404.html +0 -67
  179. data/examples/five-letter-word-game/public/422.html +0 -67
  180. data/examples/five-letter-word-game/public/500.html +0 -66
  181. data/examples/five-letter-word-game/public/apple-touch-icon-precomposed.png +0 -0
  182. data/examples/five-letter-word-game/public/apple-touch-icon.png +0 -0
  183. data/examples/five-letter-word-game/public/favicon.ico +0 -0
  184. data/examples/five-letter-word-game/public/robots.txt +0 -5
  185. data/examples/five-letter-word-game/test/controllers/.keep +0 -0
  186. data/examples/five-letter-word-game/test/fixtures/.keep +0 -0
  187. data/examples/five-letter-word-game/test/fixtures/files/.keep +0 -0
  188. data/examples/five-letter-word-game/test/helpers/.keep +0 -0
  189. data/examples/five-letter-word-game/test/integration/.keep +0 -0
  190. data/examples/five-letter-word-game/test/mailers/.keep +0 -0
  191. data/examples/five-letter-word-game/test/models/.keep +0 -0
  192. data/examples/five-letter-word-game/test/test_helper.rb +0 -10
  193. data/examples/five-letter-word-game/tmp/.keep +0 -0
  194. data/examples/five-letter-word-game/vendor/assets/javascripts/.keep +0 -0
  195. data/examples/five-letter-word-game/vendor/assets/stylesheets/.keep +0 -0
  196. data/examples/smoke_test/.gitignore +0 -21
  197. data/examples/smoke_test/Gemfile +0 -59
  198. data/examples/smoke_test/Gemfile.lock +0 -289
  199. data/examples/smoke_test/README.md +0 -24
  200. data/examples/smoke_test/Rakefile +0 -6
  201. data/examples/smoke_test/app/assets/config/manifest.js +0 -3
  202. data/examples/smoke_test/app/assets/images/.keep +0 -0
  203. data/examples/smoke_test/app/assets/javascripts/application.js +0 -15
  204. data/examples/smoke_test/app/assets/javascripts/cable.js +0 -13
  205. data/examples/smoke_test/app/assets/javascripts/channels/.keep +0 -0
  206. data/examples/smoke_test/app/assets/stylesheets/application.css +0 -15
  207. data/examples/smoke_test/app/channels/application_cable/channel.rb +0 -4
  208. data/examples/smoke_test/app/channels/application_cable/connection.rb +0 -4
  209. data/examples/smoke_test/app/controllers/app_controller.rb +0 -5
  210. data/examples/smoke_test/app/controllers/application_controller.rb +0 -3
  211. data/examples/smoke_test/app/helpers/application_helper.rb +0 -2
  212. data/examples/smoke_test/app/hyperloop/components/hello.rb +0 -25
  213. data/examples/smoke_test/app/hyperloop/operations/operations/nested_send_to_all.rb +0 -7
  214. data/examples/smoke_test/app/hyperloop/operations/send_to_all.rb +0 -6
  215. data/examples/smoke_test/app/hyperloop/stores/messages.rb +0 -4
  216. data/examples/smoke_test/app/jobs/application_job.rb +0 -2
  217. data/examples/smoke_test/app/mailers/application_mailer.rb +0 -4
  218. data/examples/smoke_test/app/models/application_record.rb +0 -3
  219. data/examples/smoke_test/app/models/concerns/.keep +0 -0
  220. data/examples/smoke_test/app/policies/application_policy.rb +0 -8
  221. data/examples/smoke_test/app/views/layouts/application.html.erb +0 -14
  222. data/examples/smoke_test/app/views/layouts/mailer.html.erb +0 -13
  223. data/examples/smoke_test/app/views/layouts/mailer.text.erb +0 -1
  224. data/examples/smoke_test/bin/bundle +0 -3
  225. data/examples/smoke_test/bin/rails +0 -9
  226. data/examples/smoke_test/bin/rake +0 -9
  227. data/examples/smoke_test/bin/setup +0 -34
  228. data/examples/smoke_test/bin/spring +0 -17
  229. data/examples/smoke_test/bin/update +0 -29
  230. data/examples/smoke_test/config.ru +0 -5
  231. data/examples/smoke_test/config/application.rb +0 -15
  232. data/examples/smoke_test/config/boot.rb +0 -3
  233. data/examples/smoke_test/config/cable.yml +0 -9
  234. data/examples/smoke_test/config/database.yml +0 -25
  235. data/examples/smoke_test/config/environment.rb +0 -5
  236. data/examples/smoke_test/config/environments/development.rb +0 -54
  237. data/examples/smoke_test/config/environments/production.rb +0 -86
  238. data/examples/smoke_test/config/environments/test.rb +0 -42
  239. data/examples/smoke_test/config/initializers/application_controller_renderer.rb +0 -6
  240. data/examples/smoke_test/config/initializers/assets.rb +0 -11
  241. data/examples/smoke_test/config/initializers/backtrace_silencers.rb +0 -7
  242. data/examples/smoke_test/config/initializers/cookies_serializer.rb +0 -5
  243. data/examples/smoke_test/config/initializers/filter_parameter_logging.rb +0 -4
  244. data/examples/smoke_test/config/initializers/hyperloop.rb +0 -32
  245. data/examples/smoke_test/config/initializers/inflections.rb +0 -16
  246. data/examples/smoke_test/config/initializers/mime_types.rb +0 -4
  247. data/examples/smoke_test/config/initializers/new_framework_defaults.rb +0 -24
  248. data/examples/smoke_test/config/initializers/session_store.rb +0 -3
  249. data/examples/smoke_test/config/initializers/wrap_parameters.rb +0 -14
  250. data/examples/smoke_test/config/locales/en.yml +0 -23
  251. data/examples/smoke_test/config/puma.rb +0 -47
  252. data/examples/smoke_test/config/routes.rb +0 -5
  253. data/examples/smoke_test/config/secrets.yml +0 -22
  254. data/examples/smoke_test/config/spring.rb +0 -6
  255. data/examples/smoke_test/db/seeds.rb +0 -7
  256. data/examples/smoke_test/lib/assets/.keep +0 -0
  257. data/examples/smoke_test/lib/tasks/.keep +0 -0
  258. data/examples/smoke_test/log/.keep +0 -0
  259. data/examples/smoke_test/public/404.html +0 -67
  260. data/examples/smoke_test/public/422.html +0 -67
  261. data/examples/smoke_test/public/500.html +0 -66
  262. data/examples/smoke_test/public/apple-touch-icon-precomposed.png +0 -0
  263. data/examples/smoke_test/public/apple-touch-icon.png +0 -0
  264. data/examples/smoke_test/public/favicon.ico +0 -0
  265. data/examples/smoke_test/public/robots.txt +0 -5
  266. data/examples/smoke_test/test/controllers/.keep +0 -0
  267. data/examples/smoke_test/test/fixtures/.keep +0 -0
  268. data/examples/smoke_test/test/fixtures/files/.keep +0 -0
  269. data/examples/smoke_test/test/helpers/.keep +0 -0
  270. data/examples/smoke_test/test/integration/.keep +0 -0
  271. data/examples/smoke_test/test/mailers/.keep +0 -0
  272. data/examples/smoke_test/test/models/.keep +0 -0
  273. data/examples/smoke_test/test/test_helper.rb +0 -10
  274. data/examples/smoke_test/tmp/.keep +0 -0
  275. data/examples/smoke_test/vendor/assets/javascripts/.keep +0 -0
  276. data/examples/smoke_test/vendor/assets/stylesheets/.keep +0 -0
  277. data/lib/hyper-operation/call_by_class_name.rb +0 -60
@@ -68,6 +68,7 @@ module Hyperloop
68
68
  end
69
69
 
70
70
  def hash_filter
71
+ # the :duck method is added in lib/hyper-operation.rb globally
71
72
  @hash_filter ||= Mutations::HashFilter.new
72
73
  end
73
74
 
@@ -1,37 +1,99 @@
1
+ require 'net/http' unless RUBY_ENGINE == 'opal'
2
+
1
3
  module Hyperloop
2
4
  class ServerOp < Operation
3
5
 
4
6
  class << self
5
- def run(*args)
6
- hash = _Railway.params_wrapper.combine_arg_array(args)
7
- hash = serialize_params(hash)
8
- HTTP.post(
9
- "#{`window.HyperloopEnginePath`}/execute_remote",
10
- payload: {json: {operation: name, params: hash}.to_json},
11
- headers: {'X-CSRF-Token' => Hyperloop::ClientDrivers.opts[:form_authenticity_token] }
12
- )
13
- .then do |response|
14
- deserialize_response response.json[:response]
15
- end.fail do |response|
16
- Exception.new response.json[:error]
7
+ include React::IsomorphicHelpers
8
+
9
+ if RUBY_ENGINE == 'opal'
10
+ if on_opal_client?
11
+ def run(*args)
12
+ hash = _Railway.params_wrapper.combine_arg_array(args)
13
+ hash = serialize_params(hash)
14
+ Hyperloop::HTTP.post(
15
+ "#{`window.HyperloopEnginePath`}/execute_remote",
16
+ payload: {json: {operation: name, params: hash}.to_json},
17
+ headers: {'X-CSRF-Token' => Hyperloop::ClientDrivers.opts[:form_authenticity_token] }
18
+ )
19
+ .then do |response|
20
+ deserialize_response response.json[:response]
21
+ end
22
+ .fail do |response|
23
+ Exception.new response.json[:error]
24
+ end
25
+ end
26
+ elsif on_opal_server?
27
+ def run(*args)
28
+ promise = Promise.new
29
+ response = internal_iso_run(name, args)
30
+ if response[:json][:response]
31
+ promise.resolve(response[:json][:response])
32
+ else
33
+ promise.reject Exception.new response[:json][:error]
34
+ end
35
+ promise
36
+ end
17
37
  end
18
- end if RUBY_ENGINE == 'opal'
38
+ end
39
+
40
+ isomorphic_method(:internal_iso_run) do |f, klass_name, op_params|
41
+ f.send_to_server(klass_name, op_params)
42
+ f.when_on_server {
43
+ Hyperloop::ServerOp.run_from_client(:acting_user, controller, klass_name, *op_params)
44
+ }
45
+ end
46
+
47
+ def descendants_map_cache
48
+ # calling descendants alone may take 10ms in a complex app, so better cache it
49
+ @cached_descendants ||= Hyperloop::ServerOp.descendants.map(&:to_s)
50
+ end
19
51
 
20
52
  def run_from_client(security_param, controller, operation, params)
53
+ if Rails.env.production?
54
+ # in production everything is eager loaded so ServerOp.descendants is filled and can be used to guard the .constantize
55
+ unless Hyperloop::ServerOp.descendants_map_cache.include?(operation)
56
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (in production)")
57
+ end
58
+ # however ...
59
+ else
60
+ # ... in development things are autoloaded on demand, thus ServerOp.descendants can be empty or partially filled and above guard
61
+ # would fail legal operations. To prevent this, the class has to be loaded first, what .const_get will take care of, and then
62
+ # its guarded, to achieve similar behaviour as in production. Doing the const_get first, before the guard,
63
+ # would not be safe for production and allow for potential remote code execution!
64
+ begin
65
+ const = Object.const_get(operation)
66
+ rescue NameError
67
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (const not found)")
68
+ end
69
+ unless const < Hyperloop::ServerOp
70
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (not a ServerOp subclass)")
71
+ end
72
+ end
21
73
  operation.constantize.class_eval do
22
74
  if _Railway.params_wrapper.method_defined?(:controller)
23
75
  params[:controller] = controller
24
76
  elsif !_Railway.params_wrapper.method_defined?(security_param)
25
77
  raise AccessViolation
26
78
  end
27
- run(params)
79
+ run(deserialize_params(params))
28
80
  .then { |r| return { json: { response: serialize_response(r) } } }
29
- .fail { |e| return { json: { error: e }, status: 500 } }
81
+ .fail { |e| return handle_exception(e, operation, params) }
30
82
  end
31
83
  rescue Exception => e
32
- { json: {error: e}, status: 500 }
84
+ handle_exception(e, operation, params)
33
85
  end
34
86
 
87
+ def handle_exception(e, operation, params)
88
+ if defined? ::Rails
89
+ params.delete(:controller)
90
+ ::Rails.logger.debug "\033[0;31;1mERROR: Hyperloop::ServerOp exception caught when running "\
91
+ "#{operation} with params \"#{params}\": #{e}\033[0;30;21m"
92
+ end
93
+ { json: { error: e }, status: 500 }
94
+ end
95
+
96
+
35
97
  def remote(path, *args)
36
98
  promise = Promise.new
37
99
  uri = URI("#{path}execute_remote_api")
@@ -39,7 +101,6 @@ module Hyperloop
39
101
  request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
40
102
  if uri.scheme == 'https'
41
103
  http.use_ssl = true
42
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
43
104
  end
44
105
  request.body = {
45
106
  operation: name,
@@ -86,9 +147,13 @@ module Hyperloop
86
147
  end
87
148
  regulation ||= proc { args }
88
149
  on_dispatch do |params, operation|
150
+ operation.instance_variable_set(:@_dispatched_channels, []) unless operation.instance_variable_get(:@_dispatched_channels)
89
151
  serialized_params = serialize_dispatch(params.to_h)
90
152
  [operation.instance_exec(*context, &regulation)].flatten.compact.uniq.each do |channel|
91
- Hyperloop.dispatch(channel: Hyperloop::InternalPolicy.channel_to_string(channel), operation: operation.class.name, params: serialized_params)
153
+ unless operation.instance_variable_get(:@_dispatched_channels).include?(channel)
154
+ operation.instance_variable_set(:@_dispatched_channels, operation.instance_variable_get(:@_dispatched_channels) << channel)
155
+ Hyperloop.dispatch(channel: Hyperloop::InternalPolicy.channel_to_string(channel), operation: operation.class.name, params: serialized_params)
156
+ end
92
157
  end
93
158
  end
94
159
  end if RUBY_ENGINE != 'opal'
@@ -7,8 +7,25 @@ module Hyperloop
7
7
  # client interface to sync_change or sync_destroy
8
8
 
9
9
  class Application
10
- def self.acting_user_id
11
- ClientDrivers.opts[:acting_user_id]
10
+ extend React::IsomorphicHelpers::ClassMethods
11
+
12
+ if on_opal_client?
13
+ def self.acting_user_id
14
+ ClientDrivers.opts[:acting_user_id]
15
+ end
16
+ else
17
+ def self.acting_user_id
18
+ ClientDrivers.client_drivers_get_acting_user_id
19
+ end
20
+ end
21
+
22
+ def self.env
23
+ @env = ClientDrivers.env unless @env
24
+ @env
25
+ end
26
+
27
+ def self.production?
28
+ env == 'production'
12
29
  end
13
30
  end
14
31
 
@@ -59,8 +76,9 @@ module Hyperloop
59
76
  }
60
77
  elsif ClientDrivers.opts[:transport] == :action_cable
61
78
  channel = "#{ClientDrivers.opts[:channel]}-#{channel_string}"
62
- HTTP.post(ClientDrivers.polling_path('action-cable-auth', channel)).then do |response|
79
+ Hyperloop::HTTP.post(ClientDrivers.polling_path('action-cable-auth', channel), headers: { 'X-CSRF-Token' => ClientDrivers.opts[:form_authenticity_token] }).then do |response|
63
80
  %x{
81
+ var fix_opal_0110 = 'return';
64
82
  #{Hyperloop.action_cable_consumer}.subscriptions.create(
65
83
  {
66
84
  channel: "Hyperloop::ActionCableChannel",
@@ -71,9 +89,11 @@ module Hyperloop
71
89
  },
72
90
  {
73
91
  connected: function() {
74
- #{ClientDrivers.get_queued_data("connect-to-transport", channel_string)}
92
+ if (#{ClientDrivers.env == 'development'}) { console.log("ActionCable connected to: ", channel_string); }
93
+ #{ClientDrivers.complete_connection(channel_string)}
75
94
  },
76
95
  received: function(data) {
96
+ if (#{ClientDrivers.env == 'development'}) { console.log("ActionCable received: ", data); }
77
97
  #{ClientDrivers.sync_dispatch(JSON.parse(`JSON.stringify(data)`)['data'])}
78
98
  }
79
99
  }
@@ -81,7 +101,7 @@ module Hyperloop
81
101
  }
82
102
  end
83
103
  else
84
- HTTP.get(ClientDrivers.polling_path(:subscribe, channel_string))
104
+ Hyperloop::HTTP.get(ClientDrivers.polling_path(:subscribe, channel_string))
85
105
  end
86
106
  end
87
107
  end
@@ -105,24 +125,26 @@ module Hyperloop
105
125
  # will remove the session from the list.
106
126
 
107
127
  prerender_footer do |controller|
108
- next if Hyperloop.transport == :none
109
- if defined?(PusherFake)
110
- path = ::Rails.application.routes.routes.detect do |route|
111
- route.app == Hyperloop::Engine ||
112
- (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
113
- end.path.spec
114
- pusher_fake_js = PusherFake.javascript(
115
- auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
116
- authEndpoint: "#{path}/hyperloop-pusher-auth"
117
- )
128
+ unless Hyperloop.transport == :none
129
+ if defined?(PusherFake)
130
+ path = ::Rails.application.routes.routes.detect do |route|
131
+ route.app == Hyperloop::Engine ||
132
+ (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
133
+ end.path.spec
134
+ pusher_fake_js = PusherFake.javascript(
135
+ auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
136
+ authEndpoint: "#{path}/hyperloop-pusher-auth"
137
+ )
138
+ end
139
+ controller.session.delete 'hyperloop-dummy-init' unless controller.session.id
140
+ id = "#{SecureRandom.uuid}-#{controller.session.id}"
141
+ auto_connections = Hyperloop::AutoConnect.channels(id, controller.acting_user)
118
142
  end
119
- controller.session.delete 'hyperloop-dummy-init' unless controller.session.id
120
- id = "#{SecureRandom.uuid}-#{controller.session.id}"
121
- auto_connections = Hyperloop::AutoConnect.channels(id, controller.acting_user)
122
143
  config_hash = {
123
144
  transport: Hyperloop.transport,
124
145
  id: id,
125
146
  acting_user_id: (controller.acting_user && controller.acting_user.id),
147
+ env: ::Rails.env,
126
148
  client_logging: Hyperloop.client_logging,
127
149
  pusher_fake_js: pusher_fake_js,
128
150
  key: Hyperloop.key,
@@ -149,9 +171,27 @@ module Hyperloop
149
171
  attr_reader :opts
150
172
  end
151
173
 
174
+ isomorphic_method(:client_drivers_get_acting_user_id) do |f|
175
+ f.send_to_server if RUBY_ENGINE == 'opal'
176
+ f.when_on_server { (controller.acting_user && controller.acting_user.id) }
177
+ end
178
+
179
+ isomorphic_method(:env) do |f|
180
+ f.when_on_client { opts[:env] }
181
+ f.send_to_server
182
+ f.when_on_server { ::Rails.env }
183
+ end
184
+
185
+ def self.complete_connection(channel, retries = 10)
186
+ get_queued_data('connect-to-transport', channel).fail do
187
+ after(0.25) { complete_connection(channel, retries - 1) } unless retries.zero?
188
+ end
189
+ end
190
+
152
191
  def self.get_queued_data(operation, channel = nil, opts = {})
153
- HTTP.get(polling_path(operation, channel), opts).then do |response|
192
+ Hyperloop::HTTP.get(polling_path(operation, channel), opts).then do |response|
154
193
  response.json.each do |data|
194
+ `console.log("simple_poller received: ", data)` if ClientDrivers.env == 'development'
155
195
  sync_dispatch(data[1])
156
196
  end
157
197
  end
@@ -161,21 +201,24 @@ module Hyperloop
161
201
 
162
202
  if @initialized
163
203
  # 1) skip initialization if already initialized
164
- # 2) if running action_cable make sure connection is up after pinging the server_up
165
- # action cable closes the connection if files change on the server
166
- HTTP.get("#{`window.HyperloopEnginePath`}/server_up") do
167
- `#{Hyperloop.action_cable_consumer}.connection.open()` if `#{Hyperloop.action_cable_consumer}.connection.disconnected`
168
- end if Hyperloop.action_cable_consumer
204
+ if on_opal_client? && Hyperloop.action_cable_consumer
205
+ # 2) if running action_cable make sure connection is up after pinging the server_up
206
+ # action cable closes the connection if files change on the server
207
+ Hyperloop::HTTP.get("#{`window.HyperloopEnginePath`}/server_up") do
208
+ `#{Hyperloop.action_cable_consumer}.connection.open()` if `#{Hyperloop.action_cable_consumer}.connection.disconnected`
209
+ end
210
+ end
169
211
  return
170
212
  end
171
213
 
172
214
  @initialized = true
215
+ @opts = {}
216
+
217
+ if on_opal_client?
173
218
 
174
- if RUBY_ENGINE == 'opal'
175
219
  @opts = Hash.new(`window.HyperloopOpts`)
176
- end
177
220
 
178
- if on_opal_client?
221
+
179
222
  if opts[:transport] == :pusher
180
223
 
181
224
  opts[:dispatch] = lambda do |data|
@@ -209,7 +252,7 @@ module Hyperloop
209
252
  elsif opts[:transport] == :simple_poller
210
253
  opts[:auto_connect].each { |channel| IncomingBroadcast.add_connection(*channel) }
211
254
  every(opts[:seconds_between_poll]) do
212
- get_queued_data(:read, nil, headers: {'X-HYPERLOOP-SILENT-REQUEST' => true })
255
+ get_queued_data(:read, nil)
213
256
  end
214
257
  end
215
258
  end
@@ -1,20 +1,16 @@
1
1
  module Hyperloop
2
2
  module AutoCreate
3
- def needs_init?
4
- return false if Hyperloop.transport == :none
5
- return true if connection.respond_to?(:data_sources) && !connection.data_sources.include?(table_name)
6
- return true if !connection.respond_to?(:data_sources) && !connection.tables.include?(table_name)
7
- return false unless Hyperloop.on_server?
8
- return true if defined?(Rails::Server)
9
- return true unless Connection.root_path
10
- uri = URI("#{Connection.root_path}server_up")
11
- http = Net::HTTP.new(uri.host, uri.port)
12
- request = Net::HTTP::Get.new(uri.path)
13
- if uri.scheme == 'https'
14
- http.use_ssl = true
15
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
3
+ def table_exists?
4
+ # works with both rails 4 and 5 without deprecation warnings
5
+ if connection.respond_to?(:data_sources)
6
+ connection.data_sources.include?(table_name)
7
+ else
8
+ connection.tables.include?(table_name)
16
9
  end
17
- http.request(request) && return rescue true
10
+ end
11
+
12
+ def needs_init?
13
+ Hyperloop.transport != :none && Hyperloop.on_server? && !table_exists?
18
14
  end
19
15
 
20
16
  def create_table(*args, &block)
@@ -58,14 +54,14 @@ module Hyperloop
58
54
  extend AutoCreate
59
55
 
60
56
  def self.build_tables
61
- create_table(force: true) do |t|
57
+ create_table(force: :cascade) do |t|
62
58
  t.string :channel
63
59
  t.string :session
64
60
  t.datetime :created_at
65
61
  t.datetime :expires_at
66
62
  t.datetime :refresh_at
67
63
  end
68
- QueuedMessage.create_table(force: true) do |t|
64
+ QueuedMessage.create_table(force: :cascade) do |t|
69
65
  t.text :data
70
66
  t.integer :connection_id
71
67
  end
@@ -106,6 +102,10 @@ module Hyperloop
106
102
  attr_accessor :transport
107
103
 
108
104
  def active
105
+ # if table doesn't exist then we are either calling from within
106
+ # a migration or from a console before the server has ever started
107
+ # in these cases there are no channels so we return nothing
108
+ return [] unless table_exists?
109
109
  if Hyperloop.on_server?
110
110
  expired.delete_all
111
111
  refresh_connections if needs_refresh?
@@ -153,9 +153,10 @@ module Hyperloop
153
153
  end
154
154
 
155
155
  def root_path
156
- QueuedMessage.root_path
157
- rescue
158
- nil
156
+ # if the QueuedMessage table doesn't exist then we are either calling from within
157
+ # a migration or from a console before the server has ever started
158
+ # in these cases there is no root path to the server
159
+ QueuedMessage.root_path if QueuedMessage.table_exists?
159
160
  end
160
161
 
161
162
  def refresh_connections
@@ -163,7 +164,8 @@ module Hyperloop
163
164
  channels = transport.refresh_channels
164
165
  next_refresh = refresh_started_at + transport.refresh_channels_every
165
166
  channels.each do |channel|
166
- find_by(channel: channel, session: nil).update(refresh_at: next_refresh)
167
+ connection = find_by(channel: channel, session: nil)
168
+ connection.update(refresh_at: next_refresh) if connection
167
169
  end
168
170
  inactive.delete_all
169
171
  end
@@ -122,7 +122,7 @@ module Hyperloop
122
122
  end
123
123
 
124
124
  def self.on_server?
125
- Rails.const_defined? 'Server'
125
+ return defined? Rails::Server
126
126
  end
127
127
 
128
128
  def self.pusher
@@ -17,7 +17,7 @@ module Hyperloop
17
17
  unless method_defined? :pre_hyperloop_call
18
18
  alias pre_hyperloop_call call
19
19
  def call(env)
20
- if !Hyperloop.opts[:noisy] && env['HTTP_X_HYPERLOOP_SILENT_REQUEST']
20
+ if Hyperloop.transport == :simple_poller && env['PATH_INFO'] && env['PATH_INFO'].include?('/hyperloop-read/')
21
21
  Rails.logger.silence do
22
22
  pre_hyperloop_call(env)
23
23
  end
@@ -115,6 +115,7 @@ module Hyperloop
115
115
  end
116
116
 
117
117
  def pusher_auth
118
+ raise unless Hyperloop.transport == :pusher
118
119
  channel = regulate params[:channel_name].gsub(/^#{Regexp.quote(Hyperloop.channel)}\-/,'').gsub('==', '::')
119
120
  response = Hyperloop.pusher.authenticate(params[:channel_name], params[:socket_id])
120
121
  render json: response
@@ -123,6 +124,7 @@ module Hyperloop
123
124
  end
124
125
 
125
126
  def action_cable_auth
127
+ raise unless Hyperloop.transport == :action_cable
126
128
  channel = regulate params[:channel_name].gsub(/^#{Regexp.quote(Hyperloop.channel)}\-/,'')
127
129
  salt = SecureRandom.hex
128
130
  authorization = Hyperloop.authorization(salt, channel, client_id)
@@ -134,6 +136,8 @@ module Hyperloop
134
136
  def connect_to_transport
135
137
  root_path = request.original_url.gsub(/hyperloop-connect-to-transport.*$/, '')
136
138
  render json: Hyperloop::Connection.connect_to_transport(params[:channel], client_id, root_path)
139
+ rescue Exception => e
140
+ render status: :service_unavailable, json: {error: e}
137
141
  end
138
142
 
139
143
  def execute_remote
@@ -154,6 +158,7 @@ module Hyperloop
154
158
  end
155
159
 
156
160
  def console_update # TODO this should just become an execute-remote-api call
161
+ raise unless Rails.env.development?
157
162
  authorization = Hyperloop.authorization(params[:salt], params[:channel], params[:data][1][:broadcast_id]) #params[:data].to_json)
158
163
  return head :unauthorized if authorization != params[:authorization]
159
164
  Hyperloop::Connection.send_to_channel(params[:channel], params[:data])
@@ -3,6 +3,16 @@ module Hyperloop
3
3
  class InternalClassPolicy
4
4
 
5
5
  def initialize(regulated_klass)
6
+ unless regulated_klass.is_a?(Class)
7
+ # attempt to constantize the class in case eager_loading in production
8
+ # has loaded the policy before the class. THis will insure that if
9
+ # there is a class being regulated, it is loaded first.
10
+ begin
11
+ regulated_klass.constantize
12
+ rescue NameError
13
+ nil
14
+ end
15
+ end
6
16
  @regulated_klass = regulated_klass
7
17
  end
8
18
 
@@ -49,11 +59,17 @@ module Hyperloop
49
59
  end
50
60
 
51
61
  def dispatch_to(*args, &regulation)
52
- actual_klass = regulated_klass.is_a?(Class) ? regulated_klass : regulated_klass.constantize rescue nil
53
- actual_klass.dispatch_to(actual_klass) if actual_klass.respond_to? :dispatch_to
54
- unless actual_klass.respond_to? :dispatch_to
55
- raise 'you can only dispatch_to Operation classes'
56
- end
62
+ actual_klass = if regulated_klass.is_a?(Class)
63
+ regulated_klass
64
+ else
65
+ begin
66
+ regulated_klass.constantize
67
+ rescue NameError
68
+ nil
69
+ end
70
+ end
71
+ raise 'you can only dispatch_to Operation classes' unless actual_klass.respond_to? :dispatch_to
72
+ actual_klass.dispatch_to(actual_klass)
57
73
  actual_klass.dispatch_to(*args, &regulation)
58
74
  end
59
75
 
@@ -82,14 +98,40 @@ module Hyperloop
82
98
  end
83
99
  end
84
100
 
101
+ def self.ar_base_descendants_map_cache
102
+ @ar_base_descendants_map_cache ||= ActiveRecord::Base.descendants.map(&:name)
103
+ end
104
+
85
105
  def get_ar_model(str)
86
- str.is_a?(Class) ? str : Object.const_get(str)
87
- rescue
88
- raise "#{str} is not a class"
106
+ if str.is_a?(Class)
107
+ unless str <= ActiveRecord::Base
108
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:non_ar_class, "#{str} is not a subclass of ActiveRecord::Base")
109
+ end
110
+ str
111
+ else
112
+ # we used to cache this here, but during eager loading the cache may get partially filled and never updated
113
+ # so this guard will fail, now performance will be suckish, as this guard, required for security, takes some ms
114
+ # def self.ar_base_descendants_map_cache
115
+ # @ar_base_descendants_map_cache ||= ActiveRecord::Base.descendants.map(&:name)
116
+ # end
117
+ # if Rails.env.production? && !Hyperloop::InternalClassPolicy.ar_base_descendants_map_cache.include?(str)
118
+ if Rails.application.config.eager_load && !ActiveRecord::Base.descendants.map(&:name).include?(str)
119
+ # AR::Base.descendants is eager loaded in production -> this guard works.
120
+ # In development it may be empty or partially filled -> this guard may fail.
121
+ # Thus guarded here only in production.
122
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:non_ar_class, "#{str} is either not defined or is not a subclass of ActiveRecord::Base")
123
+ end
124
+ Object.const_get(str)
125
+ end
126
+ end
127
+
128
+ def self.regulated_klasses
129
+ @regulated_klasses ||= Set.new
89
130
  end
90
131
 
91
132
  def regulate(regulation_klass, policy, args, &regulation)
92
133
  process_args(policy, regulation_klass.allowed_opts, args, regulation) do |regulated_klass, opts|
134
+ self.class.regulated_klasses << regulated_klass.to_s
93
135
  regulation_klass.add_regulation regulated_klass, opts, &regulation
94
136
  end
95
137
  end
@@ -201,8 +243,23 @@ module Hyperloop
201
243
  class ClassConnectionRegulation < Regulation
202
244
 
203
245
  def self.add_regulation(klass, opts={}, &regulation)
204
- actual_klass = klass.is_a?(Class) ? klass : klass.constantize rescue nil
205
- actual_klass.dispatch_to(actual_klass) if actual_klass.respond_to? :dispatch_to rescue nil
246
+ actual_klass = if klass.is_a?(Class)
247
+ klass
248
+ else
249
+ begin
250
+ klass.constantize
251
+ rescue NameError
252
+ nil
253
+ end
254
+ end
255
+ if actual_klass && actual_klass.respond_to?(:dispatch_to)
256
+ begin
257
+ actual_klass.dispatch_to(actual_klass)
258
+ rescue NoMethodError
259
+ # this is the case for ClassPolicy where the instance method :dispatch_to has been deleted.
260
+ nil
261
+ end
262
+ end
206
263
  super
207
264
  end
208
265
 
@@ -321,10 +378,18 @@ module Hyperloop
321
378
  @obj
322
379
  end
323
380
 
381
+ def self.raise_operation_access_violation(message, details)
382
+ Hyperloop.on_error(Hyperloop::AccessViolation, message, details)
383
+ raise Hyperloop::AccessViolation
384
+ end
385
+
324
386
  def self.regulate_connection(acting_user, channel_string)
325
387
  channel = channel_string.split("-")
326
388
  if channel.length > 1
327
389
  id = channel[1..-1].join("-")
390
+ unless Hyperloop::InternalClassPolicy.regulated_klasses.include?(channel[0])
391
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:not_a_channel, "#{channel[0]} is not regulated channel class")
392
+ end
328
393
  object = Object.const_get(channel[0]).find(id)
329
394
  InstanceConnectionRegulation.connect(object, acting_user)
330
395
  else
@@ -486,7 +551,7 @@ module Hyperloop
486
551
 
487
552
  module PolicyAutoLoader
488
553
  def self.load(name, value)
489
- const_get("#{name}Policy") if name && !(name =~ /Policy$/) && value.is_a?(Class)
554
+ const_get("#{name}Policy") if name && !name.end_with?("Policy".freeze) && value.is_a?(Class)
490
555
  rescue Exception => e
491
556
  raise e if e.is_a?(LoadError) && e.message =~ /Unable to autoload constant #{name}Policy/
492
557
  end
@@ -515,8 +580,8 @@ class Class
515
580
 
516
581
  Hyperloop::ClassPolicyMethods.instance_methods.each do |method|
517
582
  define_method method do |*args, &block|
518
- if name =~ /Policy$/
519
- @hyperloop_internal_policy_object = Hyperloop::InternalClassPolicy.new(name.gsub(/Policy$/,""))
583
+ if name.end_with?("Policy".freeze)
584
+ @hyperloop_internal_policy_object = Hyperloop::InternalClassPolicy.new(name.sub(/Policy$/,""))
520
585
  include Hyperloop::PolicyMethods
521
586
  send method, *args, &block
522
587
  else