landline 0.10.0 → 0.12.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.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Landline
6
+ # Module for controlling session signing secrets
7
+ module Session
8
+ # Set hmac secret
9
+ # @param secret [String]
10
+ def self.hmac_secret=(secret)
11
+ @hmac_secret = secret
12
+ end
13
+
14
+ # Get hmac secret
15
+ def self.hmac_secret
16
+ unless @hmac_secret or ENV['HMAC_SECRET']
17
+ warn <<~MSG
18
+ warn: hmac secret not supplied, using randomized one
19
+ warn: provide hmac secret with $HMAC_SECRET or Landline::Session.hmac_secret
20
+ MSG
21
+ end
22
+ @hmac_secret ||= ENV.fetch('HMAC_SECRET', SecureRandom.base64(80))
23
+ end
24
+
25
+ # Class for representing session errors
26
+ class SessionError < ::StandardError
27
+ end
28
+
29
+ # Class for representing session storage
30
+ class Session
31
+ def initialize(cookie, cookies_callback)
32
+ @data = if cookie
33
+ Landline::Util::JWT.from_string(
34
+ cookie,
35
+ Landline::Session.hmac_secret
36
+ )
37
+ else
38
+ Landline::Util::JWT.new({})
39
+ end
40
+ @valid = !@data.nil?
41
+ @cookies_callback = cookies_callback
42
+ end
43
+
44
+ # Retrieve data from session storage
45
+ # @param key [String, Symbol] serializable key
46
+ def [](key)
47
+ raise Landline::Session::SessionError, "session not valid" unless @valid
48
+
49
+ unless key.is_a? String or key.is_a? Symbol
50
+ raise StandardError, "key not serializable"
51
+ end
52
+
53
+ @data.data[key]
54
+ end
55
+
56
+ # Set data to session storage
57
+ # @param key [String, Symbol] serializable key
58
+ # @param value [Object] serializable data
59
+ def []=(key, value)
60
+ raise Landline::Session::SessionError, "session not valid" unless @valid
61
+
62
+ unless key.is_a? String or key.is_a? Symbol
63
+ raise StandardError, "key not serializable"
64
+ end
65
+
66
+ @data.data[key] = value
67
+ @cookies_callback.call(@data.make(Landline::Session.hmac_secret))
68
+ end
69
+
70
+ attr_reader :valid
71
+ end
72
+ end
73
+ end
74
+
75
+ module Landline
76
+ module DSL
77
+ module ProbeMethods
78
+ # (in Landline::Probe context)
79
+ # Return session storage hash
80
+ # @return [Landline::Session::Session]
81
+ def session
82
+ return @session if @session
83
+
84
+ @session = Landline::Session::Session.new(
85
+ request.cookies.dig('session', 0)&.value,
86
+ proc do |value|
87
+ delete_cookie("session", value)
88
+ cookie("session", value)
89
+ end
90
+ )
91
+ request.postprocessors.append(proc do
92
+ @session = nil
93
+ end)
94
+ @session
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'websocket'
4
+ module Landline
5
+ # Module that holds websocket primitives
6
+ module WebSocket
7
+ # Event system
8
+ module Eventifier
9
+ # Attach event listener
10
+ # @param event [Symbol]
11
+ # @param listener [#call]
12
+ def on(event, &listener)
13
+ @__listeners ||= {}
14
+ @__listeners[event] ||= []
15
+ @__listeners[event].append(listener)
16
+ listener
17
+ end
18
+
19
+ # Attach event listener
20
+ # @param event [Symbol]
21
+ # @param listener [#call]
22
+ def off(event, listener)
23
+ @__listeners ||= {}
24
+ @__listeners[event]&.delete(listener)
25
+ end
26
+
27
+ # Await for an event
28
+ # @param event [Symbol, Array<Symbol>] event or array of events to wait for
29
+ # @return [Array]
30
+ # @sg-ignore
31
+ def await(event)
32
+ blocking = true
33
+ output = nil
34
+ listener = proc do |*data|
35
+ output = data
36
+ blocking = false
37
+ end
38
+ if event.is_a? Array
39
+ event.each { |x| on(x, &listener) }
40
+ else
41
+ on(event, &listener)
42
+ end
43
+ while blocking; end
44
+ return output[0] if output.is_a? Array and output.length == 1
45
+
46
+ output
47
+ end
48
+
49
+ private
50
+
51
+ # Trigger the queue clearing process
52
+ # @return [void]
53
+ def _process
54
+ return if @processing
55
+
56
+ @__processing = true
57
+ @__queue ||= []
58
+ @__listeners ||= {}
59
+ until @__queue.empty?
60
+ event, msg = @__queue.shift
61
+ if @__listeners.include? event
62
+ @__listeners[event].each { |x| x.call(*msg) }
63
+ end
64
+ end
65
+ @processing = false
66
+ end
67
+
68
+ # Send internal event
69
+ # @param event [Symbol]
70
+ # @param data [Array]
71
+ # @return [void]
72
+ def _emit(event, *data)
73
+ return unless @__listeners
74
+
75
+ return unless @__listeners.include? event
76
+
77
+ @__queue ||= []
78
+ @__queue.push([event, data])
79
+ _process
80
+ end
81
+ end
82
+
83
+ # Socket-like object representing websocket interface
84
+ class WSockWrapper
85
+ include Eventifier
86
+
87
+ class WebSocketError < StandardError
88
+ end
89
+
90
+ def initialize(io, version: 7)
91
+ @io = io
92
+ @version = version
93
+ @frame_parser = ::WebSocket::Frame::Incoming::Server.new(
94
+ version: version
95
+ )
96
+ @readable = true
97
+ @writable = true
98
+ @data = Queue.new
99
+ on :message do |msg|
100
+ @data.enq(msg)
101
+ end
102
+ end
103
+
104
+ # Start the main loop for the eventifier
105
+ # @return [void]
106
+ def ready
107
+ return if @ready
108
+
109
+ _loop
110
+ @ready = true
111
+ end
112
+
113
+ # Send data through websocket
114
+ # @param data [String] binary data
115
+ # @return [void]
116
+ def write(data, type: :text)
117
+ unless @writable
118
+ raise self.class::WebSocketError,
119
+ "socket closed for writing"
120
+ end
121
+
122
+ frame = ::WebSocket::Frame::Outgoing::Server.new(
123
+ version: @version,
124
+ data: data,
125
+ type: type
126
+ )
127
+ @io.write(frame.to_s)
128
+ end
129
+
130
+ # Read data from socket synchronously
131
+ # @return [String, nil] nil returned if socket closes
132
+ def read
133
+ unless @readable
134
+ raise self.class::WebSocketError,
135
+ "socket closed for reading"
136
+ end
137
+
138
+ @data.deq
139
+ end
140
+
141
+ # Close the socket for reading
142
+ # @return [void]
143
+ def close_read
144
+ _emit :close
145
+ @readable = false
146
+ @io.close_read
147
+ end
148
+
149
+ # Close the socket for writing
150
+ def close_write
151
+ @writable = false
152
+ @io.close_write
153
+ end
154
+
155
+ # Establish a connection through handshake
156
+ # @return [self]
157
+ def self.handshake(request, version: 7, **opts)
158
+ raise StandardError, "stream cannot be hijacked" unless request.hijack
159
+
160
+ handshake = create_handshake(request, version: version, **opts)
161
+ return nil unless handshake
162
+
163
+ io = request.hijack.call
164
+ io.sendmsg(handshake.to_s)
165
+ new(io, version: version)
166
+ end
167
+
168
+ # Initiate a handshake
169
+ def self.create_handshake(request, **opts)
170
+ handshake = ::WebSocket::Handshake::Server.new(**opts)
171
+ handshake.from_hash({
172
+ headers: request.headers,
173
+ path: request.path_info,
174
+ query: request.query.query,
175
+ body: request.body
176
+ })
177
+ return nil unless handshake.finished? and handshake.valid?
178
+
179
+ handshake
180
+ end
181
+
182
+ # Close the socket
183
+ # @return [void]
184
+ def close
185
+ _close
186
+ @writable = false
187
+ @readable = false
188
+ end
189
+
190
+ private
191
+
192
+ # Event reading loop
193
+ # @return [void]
194
+ def _loop
195
+ @thread = Thread.new do
196
+ loop do
197
+ msg = _read
198
+ if msg and [:text, :binary].include? msg.type
199
+ _emit :message, msg
200
+ elsif msg and msg.type == :close
201
+ _emit :__close, msg
202
+ break
203
+ end
204
+ end
205
+ rescue IOError => e
206
+ @writable = false
207
+ _emit :error, e
208
+ close
209
+ ensure
210
+ close_read
211
+ end
212
+ end
213
+
214
+ # Receive data through websocket
215
+ # @return [String] output from frame
216
+ def _read
217
+ while (char = @io.getc)
218
+ @frame_parser << char
219
+ frame = @frame_parser.next
220
+ return frame if frame
221
+ end
222
+ end
223
+
224
+ # Close the websocket
225
+ # @return [void]
226
+ def _close
227
+ frame = ::WebSocket::Frame::Outgoing::Server.new(
228
+ version: @version,
229
+ type: :close
230
+ )
231
+ @io.write(frame.to_s) if @writable
232
+ sleep 0.1
233
+ @io.close
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ module Landline
240
+ module Handlers
241
+ # Web socket server handler
242
+ class WebSockServer < Landline::Probe
243
+ # @param path [Object]
244
+ # @param parent [Landline::Node]
245
+ # @param params [Hash] options hash
246
+ # @param callback [#call] callback to process request within
247
+ # @option params [Integer] :version protocol version
248
+ # @option params [Array<String>] :protocols array of supported sub-protocols
249
+ # @option params [Boolean] :secure true if the server will use wss:// protocol
250
+ def initialize(path, parent:, **params, &callback)
251
+ nodeparams = params.dup
252
+ nodeparams.delete(:version)
253
+ nodeparams.delete(:protocols)
254
+ nodeparams.delete(:secure)
255
+ super(path, parent: parent, **nodeparams)
256
+ @callback = callback
257
+ @params = params
258
+ end
259
+
260
+ # Method callback on successful request navigation
261
+ # Creates a websocket from a given request
262
+ # @param request [Landline::Request]
263
+ def process(request)
264
+ @callback.call(
265
+ Landline::WebSocket::WSockWrapper.handshake(
266
+ request,
267
+ **@params
268
+ )
269
+ )
270
+ end
271
+ end
272
+ end
273
+
274
+ module DSL
275
+ module PathConstructors
276
+ # (in Landline::Path context)
277
+ # Create a new websocket handler
278
+ def websocket(path, **args, &setup)
279
+ register(Landline::Handlers::WebSockServer.new(path,
280
+ parent: @origin,
281
+ **args,
282
+ &setup))
283
+ end
284
+ end
285
+ end
286
+ end
data/lib/landline/path.rb CHANGED
@@ -4,23 +4,9 @@ require_relative 'pattern_matching'
4
4
  require_relative 'node'
5
5
  require_relative 'dsl/constructors_path'
6
6
  require_relative 'dsl/methods_path'
7
- require_relative 'dsl/methods_common'
8
- require_relative 'dsl/methods_probe'
9
- require_relative 'dsl/constructors_probe'
10
7
  require_relative 'util/lookup'
11
8
 
12
9
  module Landline
13
- # Execution context for filters and preprocessors.
14
- class ProcessorContext
15
- include Landline::DSL::CommonMethods
16
- include Landline::DSL::ProbeMethods
17
- include Landline::DSL::ProbeConstructors
18
-
19
- def initialize(path)
20
- @origin = path
21
- end
22
- end
23
-
24
10
  # Execution context for path setup block.
25
11
  class PathContext
26
12
  include Landline::DSL::PathConstructors
@@ -33,7 +19,6 @@ module Landline
33
19
 
34
20
  # Primary building block of request navigation.
35
21
  class Path < Landline::Node
36
- ProcContext = Landline::ProcessorContext
37
22
  Context = Landline::PathContext
38
23
 
39
24
  # @param path [Object] Object to generate {Landline::Pattern} from
@@ -50,7 +35,6 @@ module Landline
50
35
  # Contexts setup
51
36
  context = self.class::Context.new(self)
52
37
  context.instance_exec(&setup)
53
- @proccontext = self.class::ProcContext.new(self)
54
38
  end
55
39
 
56
40
  # Method callback on successful request navigation.
@@ -89,18 +73,28 @@ module Landline
89
73
  @filters.append(block)
90
74
  end
91
75
 
92
- attr_reader :children, :properties, :request
76
+ attr_reader :children, :properties
93
77
 
94
78
  attr_accessor :bounce, :pipeline
95
79
 
96
80
  private
97
81
 
82
+ # Create an execution context for in-path processing blocks
83
+ def get_context(request)
84
+ request.context.origin.properties.lookup = @properties
85
+ request.context
86
+ end
87
+
98
88
  # Sequentially run through all filters and drop request if one is false
99
89
  # @param request [Landline::Request]
100
90
  # @return [Boolean] true if request passed all filters
101
91
  def run_filters(request)
92
+ proccontext = get_context(request)
102
93
  @filters.each do |filter|
103
- return false unless @proccontext.instance_exec(request, &filter)
94
+ output = catch(:break) do
95
+ proccontext.instance_exec(request, &filter)
96
+ end
97
+ return false unless output
104
98
  end
105
99
  true
106
100
  end
@@ -108,9 +102,15 @@ module Landline
108
102
  # Sequentially run all preprocessors on a request
109
103
  # @param request [Landline::Request]
110
104
  def run_preprocessors(request)
105
+ proccontext = get_context(request)
111
106
  @preprocessors.each do |preproc|
112
- @proccontext.instance_exec(request, &preproc)
107
+ output = catch(:break) do
108
+ proccontext.instance_exec(request, &preproc)
109
+ true
110
+ end
111
+ return false unless output
113
112
  end
113
+ true
114
114
  end
115
115
 
116
116
  # Append postprocessors to request
@@ -125,23 +125,33 @@ module Landline
125
125
  # @return [Boolean] true if further navigation will be done
126
126
  # @raise [UncaughtThrowError] by default throws :response if no matches found.
127
127
  def process_wrapped(request)
128
- @request = request
129
128
  return false unless run_filters(request)
130
129
 
131
- run_preprocessors(request)
130
+ return false unless run_preprocessors(request)
131
+
132
132
  enqueue_postprocessors(request)
133
133
  @children.each do |x|
134
134
  value = x.go(request)
135
- return value if value
135
+ return exit_stack(request, value) if value
136
136
  end
137
137
  value = index(request)
138
- return value if value
138
+ return exit_stack(request, value) if value
139
139
 
140
- @bounce ? false : _die(404)
140
+ notfound(request)
141
141
  rescue StandardError => e
142
- _die(500, backtrace: [e.to_s] + e.backtrace)
143
- ensure
144
- @request = nil
142
+ _die(request, 500, backtrace: [e.to_s] + e.backtrace)
143
+ end
144
+
145
+ # Run enqueued postprocessors on navigation failure
146
+ # @param request [Landline::Request]
147
+ def exit_stack(request, response = nil)
148
+ request.run_postprocessors(response)
149
+ response
150
+ end
151
+
152
+ # Exit with failure or throw 404
153
+ def notfound(request)
154
+ @bounce ? exit_stack(request) : _die(request, 404)
145
155
  end
146
156
 
147
157
  # Try to perform indexing on the path if possible
@@ -165,9 +175,10 @@ module Landline
165
175
  # @param errorcode [Integer]
166
176
  # @param backtrace [Array(String), nil]
167
177
  # @raise [UncaughtThrowError] throws :finish to stop processing
168
- def _die(errorcode, backtrace: nil)
178
+ def _die(request, errorcode, backtrace: nil)
179
+ proccontext = get_context(request)
169
180
  throw :finish, [errorcode].append(
170
- *@proccontext.instance_exec(
181
+ *proccontext.instance_exec(
171
182
  errorcode,
172
183
  backtrace: backtrace,
173
184
  &(@properties["handle.#{errorcode}"] or
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../probe"
4
+
5
+ module Landline
6
+ module Handlers
7
+ # Probe that sends files from a location
8
+ class Link < Landline::Probe
9
+ # @param path [Object]
10
+ # @param parent [Landline::Node]
11
+ def initialize(path, application, parent:)
12
+ @application = application
13
+ super(path, parent: parent, filepath: true)
14
+ end
15
+
16
+ # Method callback on successful request navigation.
17
+ # Sends request over to another rack app, stripping the part of the path that was not navigated
18
+ # @param request [Landline::Request]
19
+ # @return [Array(Integer, Host{String => Object}, Object)]
20
+ def process(request)
21
+ throw :finish, @application.call(request.env)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -12,13 +12,8 @@ module Landline
12
12
  def initialize(path, **args, &exec)
13
13
  super(path, **args)
14
14
  @callback = exec
15
- @context = Landline::ProbeContext.new(self)
16
- @response = nil
17
15
  end
18
16
 
19
- attr_accessor :response
20
- attr_reader :request
21
-
22
17
  # Method callback on successful request navigation.
23
18
  # Runs block supplied with object initialization.
24
19
  # Request's #splat and #param are passed to block.
@@ -34,23 +29,32 @@ module Landline
34
29
  # @return [Boolean] true if further navigation is possible
35
30
  # @raise [UncaughtThrowError] may raise if die() is called.
36
31
  def process(request)
37
- @response = nil
32
+ origin, context = get_context(request)
33
+
38
34
  return reject(request) unless request.path.match?(/^\/?$/)
39
-
40
- @request = request
35
+
41
36
  response = catch(:break) do
42
- @context.instance_exec(*request.splat,
43
- **request.param,
44
- &@callback)
37
+ context.instance_exec(*request.splat,
38
+ **request.param,
39
+ &@callback)
45
40
  end
46
41
  return false unless response
47
-
48
- if @response and [String, File, IO].include? response.class
49
- @response.body = response
50
- throw :finish, @response
42
+
43
+ oresponse = origin.response
44
+ if oresponse and [String, File, IO].include? response.class
45
+ oresponse.body = response
46
+ throw :finish, oresponse
51
47
  end
52
48
  throw :finish, response
53
49
  end
50
+
51
+ private
52
+
53
+ # Create a context to run handler in
54
+ def get_context(request)
55
+ request.context.origin.properties.lookup = @properties
56
+ [request.context.origin, request.context]
57
+ end
54
58
  end
55
59
  end
56
60
  end
@@ -21,17 +21,7 @@ module Landline
21
21
  autoload :TRACE, "landline/probe/http_method"
22
22
  autoload :PATCH, "landline/probe/http_method"
23
23
  autoload :Serve, "landline/probe/serve_handler"
24
- end
25
-
26
- # Context that provides execution context for Probes.
27
- class ProbeContext
28
- include Landline::DSL::ProbeConstructors
29
- include Landline::DSL::ProbeMethods
30
- include Landline::DSL::CommonMethods
31
-
32
- def initialize(origin)
33
- @origin = origin
34
- end
24
+ autoload :Link, "landline/probe/crosscall_handler"
35
25
  end
36
26
 
37
27
  # Test probe. Also base for all "reactive" nodes.