landline 0.9.3 → 0.12.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.
- checksums.yaml +4 -4
- data/HACKING.md +0 -5
- data/README.md +73 -19
- data/lib/landline/app.rb +50 -0
- data/lib/landline/dsl/constructors_path.rb +21 -0
- data/lib/landline/dsl/constructors_probe.rb +4 -2
- data/lib/landline/dsl/methods_common.rb +2 -0
- data/lib/landline/dsl/methods_path.rb +46 -5
- data/lib/landline/dsl/methods_probe.rb +129 -11
- data/lib/landline/dsl/methods_template.rb +11 -1
- data/lib/landline/extensions/session.rb +98 -0
- data/lib/landline/extensions/websocket.rb +286 -0
- data/lib/landline/node.rb +3 -2
- data/lib/landline/path.rb +69 -23
- data/lib/landline/pattern_matching/glob.rb +1 -1
- data/lib/landline/pattern_matching/rematch.rb +1 -1
- data/lib/landline/probe/crosscall_handler.rb +25 -0
- data/lib/landline/probe/handler.rb +13 -17
- data/lib/landline/probe/serve_handler.rb +0 -1
- data/lib/landline/probe.rb +12 -0
- data/lib/landline/request.rb +57 -6
- data/lib/landline/response.rb +1 -1
- data/lib/landline/server.rb +43 -18
- data/lib/landline/template/erb.rb +1 -1
- data/lib/landline/template/erubi.rb +1 -1
- data/lib/landline/template.rb +8 -3
- data/lib/landline/util/cookie.rb +10 -3
- data/lib/landline/util/jwt.rb +82 -0
- data/lib/landline/util/lookup.rb +14 -0
- data/lib/landline/util/mime.rb +2 -1
- data/lib/landline/util/multipart.rb +7 -4
- data/lib/landline/util/parseutils.rb +13 -11
- data/lib/landline/util/query.rb +2 -0
- data/lib/landline.rb +5 -3
- metadata +9 -6
- data/LAYOUT.md +0 -59
@@ -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/node.rb
CHANGED
@@ -35,7 +35,7 @@ module Landline
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# Try to navigate the path. Run method callback in response.
|
38
|
-
# @param [Landline::Request]
|
38
|
+
# @param request [Landline::Request]
|
39
39
|
# @return [Boolean]
|
40
40
|
def go(request)
|
41
41
|
# rejected at pattern
|
@@ -43,7 +43,8 @@ module Landline
|
|
43
43
|
|
44
44
|
request.push_state
|
45
45
|
path, splat, param = @pattern.match(request.path)
|
46
|
-
do_filepath(request, request.path.delete_suffix(
|
46
|
+
do_filepath(request, request.path.delete_suffix('/')
|
47
|
+
.delete_suffix(path))
|
47
48
|
request.path = path
|
48
49
|
request.splat.append(*splat)
|
49
50
|
request.param.merge!(param)
|
data/lib/landline/path.rb
CHANGED
@@ -5,12 +5,16 @@ require_relative 'node'
|
|
5
5
|
require_relative 'dsl/constructors_path'
|
6
6
|
require_relative 'dsl/methods_path'
|
7
7
|
require_relative 'dsl/methods_common'
|
8
|
+
require_relative 'dsl/methods_probe'
|
9
|
+
require_relative 'dsl/constructors_probe'
|
8
10
|
require_relative 'util/lookup'
|
9
11
|
|
10
12
|
module Landline
|
11
13
|
# Execution context for filters and preprocessors.
|
12
14
|
class ProcessorContext
|
13
15
|
include Landline::DSL::CommonMethods
|
16
|
+
include Landline::DSL::ProbeMethods
|
17
|
+
include Landline::DSL::ProbeConstructors
|
14
18
|
|
15
19
|
def initialize(path)
|
16
20
|
@origin = path
|
@@ -27,8 +31,20 @@ module Landline
|
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
34
|
+
# Ephemeral proxy class to which callback execution binds
|
35
|
+
class PathExecutionOrigin
|
36
|
+
def initialize(request, properties)
|
37
|
+
@request = request
|
38
|
+
@properties = Landline::Util::LookupROProxy.new(properties)
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_accessor :response
|
42
|
+
attr_reader :request, :properties
|
43
|
+
end
|
44
|
+
|
30
45
|
# Primary building block of request navigation.
|
31
46
|
class Path < Landline::Node
|
47
|
+
ExecutionOrigin = Landline::PathExecutionOrigin
|
32
48
|
ProcContext = Landline::ProcessorContext
|
33
49
|
Context = Landline::PathContext
|
34
50
|
|
@@ -46,7 +62,6 @@ module Landline
|
|
46
62
|
# Contexts setup
|
47
63
|
context = self.class::Context.new(self)
|
48
64
|
context.instance_exec(&setup)
|
49
|
-
@proccontext = self.class::ProcContext.new(self)
|
50
65
|
end
|
51
66
|
|
52
67
|
# Method callback on successful request navigation.
|
@@ -54,21 +69,11 @@ module Landline
|
|
54
69
|
# @return [Boolean] true if further navigation will be done
|
55
70
|
# @raise [UncaughtThrowError] by default throws :response if no matches found.
|
56
71
|
def process(request)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@children.each do |x|
|
62
|
-
if (value = x.go(request))
|
63
|
-
return value
|
64
|
-
end
|
72
|
+
if @pipeline
|
73
|
+
@pipeline.call(request) { |inner_req| process_wrapped(inner_req) }
|
74
|
+
else
|
75
|
+
process_wrapped(request)
|
65
76
|
end
|
66
|
-
value = index(request)
|
67
|
-
return value if value
|
68
|
-
|
69
|
-
_die(404)
|
70
|
-
rescue StandardError => e
|
71
|
-
_die(500, backtrace: [e.to_s] + e.backtrace)
|
72
77
|
end
|
73
78
|
|
74
79
|
# Add a preprocessor to the path.
|
@@ -97,14 +102,23 @@ module Landline
|
|
97
102
|
|
98
103
|
attr_reader :children, :properties
|
99
104
|
|
105
|
+
attr_accessor :bounce, :pipeline
|
106
|
+
|
100
107
|
private
|
101
108
|
|
109
|
+
# Create an execution context for in-path processing blocks
|
110
|
+
def get_context(request)
|
111
|
+
exec_origin = self.class::ExecutionOrigin.new(request, @properties)
|
112
|
+
self.class::ProcContext.new(exec_origin)
|
113
|
+
end
|
114
|
+
|
102
115
|
# Sequentially run through all filters and drop request if one is false
|
103
116
|
# @param request [Landline::Request]
|
104
117
|
# @return [Boolean] true if request passed all filters
|
105
118
|
def run_filters(request)
|
119
|
+
proccontext = get_context(request)
|
106
120
|
@filters.each do |filter|
|
107
|
-
return false unless
|
121
|
+
return false unless proccontext.instance_exec(request, &filter)
|
108
122
|
end
|
109
123
|
true
|
110
124
|
end
|
@@ -112,8 +126,9 @@ module Landline
|
|
112
126
|
# Sequentially run all preprocessors on a request
|
113
127
|
# @param request [Landline::Request]
|
114
128
|
def run_preprocessors(request)
|
129
|
+
proccontext = get_context(request)
|
115
130
|
@preprocessors.each do |preproc|
|
116
|
-
|
131
|
+
proccontext.instance_exec(request, &preproc)
|
117
132
|
end
|
118
133
|
end
|
119
134
|
|
@@ -123,6 +138,35 @@ module Landline
|
|
123
138
|
request.postprocessors.append(*@postprocessors)
|
124
139
|
end
|
125
140
|
|
141
|
+
# Method callback on successful request navigation.
|
142
|
+
# Finds the next appropriate path to go to.
|
143
|
+
# (inner pipeline-wrapped handler)
|
144
|
+
# @return [Boolean] true if further navigation will be done
|
145
|
+
# @raise [UncaughtThrowError] by default throws :response if no matches found.
|
146
|
+
def process_wrapped(request)
|
147
|
+
return false unless run_filters(request)
|
148
|
+
|
149
|
+
run_preprocessors(request)
|
150
|
+
enqueue_postprocessors(request)
|
151
|
+
@children.each do |x|
|
152
|
+
value = x.go(request)
|
153
|
+
return exit_stack(request, value) if value
|
154
|
+
end
|
155
|
+
value = index(request)
|
156
|
+
return exit_stack(request, value) if value
|
157
|
+
|
158
|
+
@bounce ? exit_stack(request) : _die(404)
|
159
|
+
rescue StandardError => e
|
160
|
+
_die(request, 500, backtrace: [e.to_s] + e.backtrace)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Run enqueued postprocessors on navigation failure
|
164
|
+
# @param request [Landline::Request]
|
165
|
+
def exit_stack(request, response = nil)
|
166
|
+
request.run_postprocessors(response)
|
167
|
+
response
|
168
|
+
end
|
169
|
+
|
126
170
|
# Try to perform indexing on the path if possible
|
127
171
|
# @param request [Landline::Request]
|
128
172
|
# @return [Boolean] true if indexing succeeded
|
@@ -144,13 +188,15 @@ module Landline
|
|
144
188
|
# @param errorcode [Integer]
|
145
189
|
# @param backtrace [Array(String), nil]
|
146
190
|
# @raise [UncaughtThrowError] throws :finish to stop processing
|
147
|
-
def _die(errorcode, backtrace: nil)
|
191
|
+
def _die(request, errorcode, backtrace: nil)
|
192
|
+
proccontext = get_context(request)
|
148
193
|
throw :finish, [errorcode].append(
|
149
|
-
*(
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
194
|
+
*proccontext.instance_exec(
|
195
|
+
errorcode,
|
196
|
+
backtrace: backtrace,
|
197
|
+
&(@properties["handle.#{errorcode}"] or
|
198
|
+
@properties["handle.default"])
|
199
|
+
)
|
154
200
|
)
|
155
201
|
end
|
156
202
|
end
|
@@ -52,7 +52,7 @@ module Landline
|
|
52
52
|
# - underscores
|
53
53
|
# - dashes
|
54
54
|
class Glob
|
55
|
-
# @param
|
55
|
+
# @param pattern [String] Glob pattern
|
56
56
|
def initialize(pattern)
|
57
57
|
pattern = Landline::PatternMatching.canonicalize(pattern)
|
58
58
|
pieces = pattern.split(TOKENS)
|
@@ -39,7 +39,7 @@ module Landline
|
|
39
39
|
end
|
40
40
|
|
41
41
|
# Test if input is convertible and if it should be converted.
|
42
|
-
# @param input
|
42
|
+
# @param string [String] input to test conversion on
|
43
43
|
# @return [Boolean] Input can be safely converted to Glob
|
44
44
|
def self.can_convert?(string)
|
45
45
|
string.is_a? Regexp
|
@@ -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
|
@@ -7,18 +7,13 @@ module Landline
|
|
7
7
|
# Probe that executes a callback on request
|
8
8
|
class Handler < Landline::Probe
|
9
9
|
# @param path [Object]
|
10
|
-
# @param
|
10
|
+
# @param args [Hash] hashed parameters to passthrough to Probe
|
11
11
|
# @param exec [#call]
|
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
|
-
|
19
|
-
attr_accessor :response
|
20
|
-
attr_reader :request
|
21
|
-
|
16
|
+
|
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,20 +29,21 @@ 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
|
-
|
32
|
+
origin = Landline::ProbeExecutionOrigin.new(request, @properties)
|
33
|
+
context = Landline::ProbeContext.new(origin)
|
38
34
|
return reject(request) unless request.path.match?(/^\/?$/)
|
39
|
-
|
40
|
-
@request = request
|
35
|
+
|
41
36
|
response = catch(:break) do
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
context.instance_exec(*request.splat,
|
38
|
+
**request.param,
|
39
|
+
&@callback)
|
45
40
|
end
|
46
41
|
return false unless response
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
data/lib/landline/probe.rb
CHANGED
@@ -21,6 +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
|
+
autoload :Link, "landline/probe/crosscall_handler"
|
24
25
|
end
|
25
26
|
|
26
27
|
# Context that provides execution context for Probes.
|
@@ -34,6 +35,17 @@ module Landline
|
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
38
|
+
# Ephemeral proxy class to which callback execution binds
|
39
|
+
class ProbeExecutionOrigin
|
40
|
+
def initialize(request, properties)
|
41
|
+
@request = request
|
42
|
+
@properties = Landline::Util::LookupROProxy.new(properties)
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :response
|
46
|
+
attr_reader :request, :properties
|
47
|
+
end
|
48
|
+
|
37
49
|
# Test probe. Also base for all "reactive" nodes.
|
38
50
|
class Probe < Landline::Node
|
39
51
|
# @param path [Object]
|