monkrb 0.18.0 → 0.19.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +2 -2
  4. data/lib/monk/live/client/monk_live.js +2 -2
  5. data/lib/monk/live/client/protocol.js +5 -0
  6. data/lib/monk/live.rb +10 -0
  7. data/lib/monk/scaffold.rb +152 -77
  8. data/lib/monk/templates/auth/app/mailers/app_mailer.rb +22 -0
  9. data/lib/monk/templates/auth/config/auth.rb +7 -20
  10. data/lib/monk/templates/base/app/app.rb +9 -0
  11. data/lib/monk/templates/base/app/broadcasts/.keep +0 -0
  12. data/lib/monk/templates/base/app/helpers/.keep +0 -0
  13. data/lib/monk/templates/base/app/jobs/.keep +0 -0
  14. data/lib/monk/templates/base/app/mailers/.keep +0 -0
  15. data/lib/monk/templates/base/app/models/.keep +0 -0
  16. data/lib/monk/templates/base/app/presenters/.keep +0 -0
  17. data/lib/monk/templates/base/bin/websocket_server +6 -0
  18. data/lib/monk/templates/base/config/load.rb +18 -0
  19. data/lib/monk/templates/base/config.ru +3 -12
  20. data/lib/monk/templates/base/public/js/app.js +1 -1
  21. data/lib/monk/templates/jobs/{jobs → app/jobs}/send_login_link.rb +2 -1
  22. data/lib/monk/templates/jobs/bin/jobs +5 -13
  23. data/lib/monk/templates/jobs/config/jobs.rb +2 -4
  24. data/lib/monk/templates/live/{config.ru → app/app.rb} +1 -6
  25. data/lib/monk/templates/live/{views → app/views}/index.erb +2 -2
  26. data/lib/monk/templates/live/bin/websocket_server +14 -2
  27. data/lib/monk/templates/live/config/live.rb +1 -1
  28. data/lib/monk/templates/live/config/live_pg.rb +1 -1
  29. data/lib/monk/templates/mail/config/mail.rb +2 -2
  30. data/lib/monk/templates/postgres/bin/console +4 -2
  31. data/lib/monk/version.rb +1 -1
  32. data/lib/monk/websocket/errors.rb +11 -0
  33. data/lib/monk/websocket/listeners.rb +23 -0
  34. data/lib/monk/websocket/pg_fanout.rb +55 -20
  35. data/lib/monk/websocket/redis_fanout.rb +57 -14
  36. data/lib/monk/websocket/registry.rb +6 -0
  37. data/lib/monk/websocket/server.rb +77 -17
  38. metadata +19 -9
  39. /data/lib/monk/templates/auth/{views → app/views}/mail/magic_link.erb +0 -0
  40. /data/lib/monk/templates/base/{views → app/views}/index.erb +0 -0
  41. /data/lib/monk/templates/base/{views → app/views}/layouts/app.erb +0 -0
  42. /data/lib/monk/templates/jobs/{jobs → app/jobs}/hello_job.rb +0 -0
  43. /data/lib/monk/templates/live/{views → app/views}/live/_hits.erb +0 -0
@@ -25,9 +25,20 @@ module Monk
25
25
  # docs/history/chat-gap-analysis.md) -- authenticate: true only checks the
26
26
  # credential once, at the handshake, so a session revoked or
27
27
  # expired afterward would otherwise leave the socket live
28
- # indefinitely. Requires authenticate: true (there's no credential
29
- # to reverify without it) and defaults to nil (disabled), same as
30
- # ping_interval:.
28
+ # indefinitely. Requires authenticate: true or :optional (there's no
29
+ # credential to reverify without it) and defaults to nil (disabled),
30
+ # same as ping_interval:. Under :optional it watches only the
31
+ # connections that have a session.
32
+ #
33
+ # authenticate: :optional is the middle mode (docs/design/websocket.md,
34
+ # "Anonymous connections"): a connection with a valid session gets its
35
+ # subject, and one without -- no credential, or a revoked or expired
36
+ # one -- comes in anonymous (subject nil) instead of being refused.
37
+ # What an anonymous connection may then do is the handler's call:
38
+ # Monk::Live's rules deny it everything a rule doesn't open with
39
+ # `anonymous: true`. The Origin check still applies: to a cookie, as
40
+ # under true, and to an anonymous connection that sends an Origin,
41
+ # since only a browser does.
31
42
  #
32
43
  # max_payload_size: (bytes) -- see Connection::DEFAULT_MAX_PAYLOAD_SIZE
33
44
  # for what it guards against (gap 5). Unlike ping_interval:/
@@ -35,6 +46,11 @@ module Monk
35
46
  # hostile client on a public endpoint, not an opt-in behavior
36
47
  # change, so a server that doesn't mention it still gets the
37
48
  # default cap rather than no cap at all.
49
+ AUTHENTICATE_MODES = [false, true, :optional].freeze
50
+
51
+ # The signals #run treats as "stop": Ctrl+C and TERM.
52
+ STOP_SIGNALS = [Signal.list.fetch("INT"), Signal.list.fetch("TERM")].freeze
53
+
38
54
  def initialize(
39
55
  port:, bind: "0.0.0.0", allowed_origins: nil, authenticate: false, ping_interval: nil,
40
56
  reverify_interval: nil, max_payload_size: Connection::DEFAULT_MAX_PAYLOAD_SIZE
@@ -43,11 +59,15 @@ module Monk
43
59
  raise ArgumentError, "ping_interval must be positive, got #{ping_interval.inspect}"
44
60
  end
45
61
 
62
+ unless AUTHENTICATE_MODES.include?(authenticate)
63
+ raise ArgumentError, "authenticate must be false, true or :optional, got #{authenticate.inspect}"
64
+ end
65
+
46
66
  if reverify_interval
47
67
  unless reverify_interval.positive?
48
68
  raise ArgumentError, "reverify_interval must be positive, got #{reverify_interval.inspect}"
49
69
  end
50
- raise ArgumentError, "reverify_interval requires authenticate: true" unless authenticate
70
+ raise ArgumentError, "reverify_interval requires authenticate: true or :optional" unless authenticate
51
71
  end
52
72
 
53
73
  unless max_payload_size.positive?
@@ -57,8 +77,8 @@ module Monk
57
77
  if authenticate
58
78
  unless defined?(Monk::Auth) && Monk::Auth.config
59
79
  raise Monk::AuthNotConfiguredError,
60
- "Monk::WebSocket::Server.new(authenticate: true) requires Monk::Auth to already be " \
61
- "configured -- call Monk::Auth.configure first"
80
+ "Monk::WebSocket::Server.new(authenticate: #{authenticate.inspect}) requires Monk::Auth to " \
81
+ "already be configured -- call Monk::Auth.configure first"
62
82
  end
63
83
 
64
84
  # Monk::Auth's config is a plain, unfrozen Hash until this runs
@@ -107,15 +127,22 @@ module Monk
107
127
  end
108
128
  ractor.send(socket, move: true)
109
129
  end
110
- rescue Interrupt
111
- # Ctrl+C's default Ruby behavior: raise Interrupt in the thread
112
- # blocked in the syscall, here TCPServer#accept. Unrescued, that's
113
- # an unhandled exception -- Ruby prints its backtrace and exits
114
- # non-zero, which reads as a crash even though this is the normal,
115
- # intended way to stop a long-running server. Registry#ask's
116
- # Ractor::ClosedError rescue (registry.rb) guards a different
117
- # failure point in this same shutdown -- a live connection's
118
- # cleanup racing the registry Ractor's teardown -- not this one.
130
+ rescue SignalException => e
131
+ raise unless STOP_SIGNALS.include?(e.signo)
132
+
133
+ # Ctrl+C (INT, raised as Interrupt) and TERM (what docker stop,
134
+ # Kubernetes, Fly, Render and systemd send on every deploy) both
135
+ # land here: Ruby raises them in the thread blocked in
136
+ # TCPServer#accept. Unrescued, INT prints a backtrace and TERM ends
137
+ # the process by the signal (exit 143) -- either reads as a crash
138
+ # though it's the normal way to stop a long-running server. Here
139
+ # the server stops accepting and #run returns, so the script ends
140
+ # with exit 0, as bin/server and bin/jobs do. Open connections are
141
+ # dropped when the process exits, with no close frame
142
+ # (docs/design/websocket.md, "Stopping the server").
143
+ # Registry#ask's Ractor::ClosedError rescue (registry.rb) guards a
144
+ # different failure point in this same shutdown -- a live
145
+ # connection's cleanup racing the registry Ractor's teardown.
119
146
  @tcp_server.close
120
147
  end
121
148
 
@@ -138,9 +165,14 @@ module Monk
138
165
  end
139
166
 
140
167
  subject = nil
141
- if authenticate
168
+ case authenticate
169
+ when true
142
170
  subject = authenticate!(socket, headers, allowed_origins)
143
171
  return unless subject
172
+ when :optional
173
+ return unless origin_allowed_for_optional?(socket, headers, allowed_origins)
174
+
175
+ subject = identify(headers)
144
176
  end
145
177
 
146
178
  socket.write(Handshake.response_for(request))
@@ -148,7 +180,7 @@ module Monk
148
180
  connection = Connection.new(socket, subject: subject, max_payload_size: max_payload_size)
149
181
  connection.start_heartbeat(ping_interval) if ping_interval
150
182
  reverify_thread =
151
- if reverify_interval
183
+ if reverify_interval && subject
152
184
  start_reverify(connection, Handshake.credential_from(headers)[:token], reverify_interval)
153
185
  end
154
186
  begin
@@ -194,6 +226,34 @@ module Monk
194
226
  end
195
227
  private_class_method :authenticate!
196
228
 
229
+ # authenticate: :optional's Origin check, before anything is verified.
230
+ # A Bearer connection is exempt, as under true. A cookie needs an
231
+ # allowed Origin, as under true. An anonymous connection that sends an
232
+ # Origin needs an allowed one too: only a browser sends it, and this
233
+ # stops another site's page from opening sockets here on its
234
+ # visitors' behalf; one that sends none (not a browser) gets in.
235
+ # Writes the 403 itself and returns false when refused.
236
+ def self.origin_allowed_for_optional?(socket, headers, allowed_origins)
237
+ return true unless allowed_origins
238
+
239
+ credential = Handshake.credential_from(headers)
240
+ return true if credential&.fetch(:via) == :bearer
241
+
242
+ origin = headers["origin"]
243
+ allowed = credential ? origin && allowed_origins.include?(origin) : origin.nil? || allowed_origins.include?(origin)
244
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n") unless allowed
245
+ allowed
246
+ end
247
+ private_class_method :origin_allowed_for_optional?
248
+
249
+ # The subject of a valid session, or nil -- no credential, or one
250
+ # Monk::Auth no longer accepts, both mean anonymous under :optional.
251
+ def self.identify(headers)
252
+ credential = Handshake.credential_from(headers)
253
+ credential && Monk::Auth.verify(credential[:token])
254
+ end
255
+ private_class_method :identify
256
+
197
257
  # Sleeps in `interval`-sized steps, re-running Monk::Auth.verify
198
258
  # against the same raw credential each time, until it comes back
199
259
  # nil (revoked or expired) -- then closes the connection with the
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monkrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matteo Folin
@@ -215,35 +215,44 @@ files:
215
215
  - lib/monk/scaffold.rb
216
216
  - lib/monk/settings.rb
217
217
  - lib/monk/state_ractor.rb
218
+ - lib/monk/templates/auth/app/mailers/app_mailer.rb
219
+ - lib/monk/templates/auth/app/views/mail/magic_link.erb
218
220
  - lib/monk/templates/auth/config/auth.rb
219
221
  - lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.down.sql
220
222
  - lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.up.sql
221
- - lib/monk/templates/auth/views/mail/magic_link.erb
222
223
  - lib/monk/templates/base/.dockerignore
223
224
  - lib/monk/templates/base/.gitignore
224
225
  - lib/monk/templates/base/.ruby-version
225
226
  - lib/monk/templates/base/Dockerfile
226
227
  - lib/monk/templates/base/Gemfile
228
+ - lib/monk/templates/base/app/app.rb
229
+ - lib/monk/templates/base/app/broadcasts/.keep
230
+ - lib/monk/templates/base/app/helpers/.keep
231
+ - lib/monk/templates/base/app/jobs/.keep
232
+ - lib/monk/templates/base/app/mailers/.keep
233
+ - lib/monk/templates/base/app/models/.keep
234
+ - lib/monk/templates/base/app/presenters/.keep
235
+ - lib/monk/templates/base/app/views/index.erb
236
+ - lib/monk/templates/base/app/views/layouts/app.erb
227
237
  - lib/monk/templates/base/bin/server
228
238
  - lib/monk/templates/base/bin/websocket_server
229
239
  - lib/monk/templates/base/config.ru
240
+ - lib/monk/templates/base/config/load.rb
230
241
  - lib/monk/templates/base/config/settings.rb
231
242
  - lib/monk/templates/base/public/css/app.css
232
243
  - lib/monk/templates/base/public/js/app.js
233
- - lib/monk/templates/base/views/index.erb
234
- - lib/monk/templates/base/views/layouts/app.erb
244
+ - lib/monk/templates/jobs/app/jobs/hello_job.rb
245
+ - lib/monk/templates/jobs/app/jobs/send_login_link.rb
235
246
  - lib/monk/templates/jobs/bin/jobs
236
247
  - lib/monk/templates/jobs/config/jobs.rb
237
248
  - lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.down.sql
238
249
  - lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.up.sql
239
- - lib/monk/templates/jobs/jobs/hello_job.rb
240
- - lib/monk/templates/jobs/jobs/send_login_link.rb
250
+ - lib/monk/templates/live/app/app.rb
251
+ - lib/monk/templates/live/app/views/index.erb
252
+ - lib/monk/templates/live/app/views/live/_hits.erb
241
253
  - lib/monk/templates/live/bin/websocket_server
242
- - lib/monk/templates/live/config.ru
243
254
  - lib/monk/templates/live/config/live.rb
244
255
  - lib/monk/templates/live/config/live_pg.rb
245
- - lib/monk/templates/live/views/index.erb
246
- - lib/monk/templates/live/views/live/_hits.erb
247
256
  - lib/monk/templates/mail/Gemfile.extra
248
257
  - lib/monk/templates/mail/config/mail.rb
249
258
  - lib/monk/templates/postgres/Dockerfile
@@ -260,6 +269,7 @@ files:
260
269
  - lib/monk/websocket/errors.rb
261
270
  - lib/monk/websocket/frame.rb
262
271
  - lib/monk/websocket/handshake.rb
272
+ - lib/monk/websocket/listeners.rb
263
273
  - lib/monk/websocket/pg_fanout.rb
264
274
  - lib/monk/websocket/redis_fanout.rb
265
275
  - lib/monk/websocket/registry.rb