landline 0.10.0 → 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/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 +2 -0
- data/lib/landline/dsl/methods_common.rb +2 -0
- data/lib/landline/dsl/methods_path.rb +25 -5
- data/lib/landline/dsl/methods_probe.rb +129 -11
- data/lib/landline/dsl/methods_template.rb +1 -0
- data/lib/landline/extensions/session.rb +98 -0
- data/lib/landline/extensions/websocket.rb +286 -0
- data/lib/landline/path.rb +37 -13
- data/lib/landline/probe/crosscall_handler.rb +25 -0
- data/lib/landline/probe/handler.rb +11 -15
- 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/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 +1 -0
- data/lib/landline/util/parseutils.rb +13 -11
- data/lib/landline/util/query.rb +2 -0
- data/lib/landline.rb +4 -2
- metadata +9 -6
- data/LAYOUT.md +0 -86
@@ -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
@@ -31,8 +31,20 @@ module Landline
|
|
31
31
|
end
|
32
32
|
end
|
33
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
|
+
|
34
45
|
# Primary building block of request navigation.
|
35
46
|
class Path < Landline::Node
|
47
|
+
ExecutionOrigin = Landline::PathExecutionOrigin
|
36
48
|
ProcContext = Landline::ProcessorContext
|
37
49
|
Context = Landline::PathContext
|
38
50
|
|
@@ -50,7 +62,6 @@ module Landline
|
|
50
62
|
# Contexts setup
|
51
63
|
context = self.class::Context.new(self)
|
52
64
|
context.instance_exec(&setup)
|
53
|
-
@proccontext = self.class::ProcContext.new(self)
|
54
65
|
end
|
55
66
|
|
56
67
|
# Method callback on successful request navigation.
|
@@ -89,18 +100,25 @@ module Landline
|
|
89
100
|
@filters.append(block)
|
90
101
|
end
|
91
102
|
|
92
|
-
attr_reader :children, :properties
|
103
|
+
attr_reader :children, :properties
|
93
104
|
|
94
105
|
attr_accessor :bounce, :pipeline
|
95
106
|
|
96
107
|
private
|
97
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
|
+
|
98
115
|
# Sequentially run through all filters and drop request if one is false
|
99
116
|
# @param request [Landline::Request]
|
100
117
|
# @return [Boolean] true if request passed all filters
|
101
118
|
def run_filters(request)
|
119
|
+
proccontext = get_context(request)
|
102
120
|
@filters.each do |filter|
|
103
|
-
return false unless
|
121
|
+
return false unless proccontext.instance_exec(request, &filter)
|
104
122
|
end
|
105
123
|
true
|
106
124
|
end
|
@@ -108,8 +126,9 @@ module Landline
|
|
108
126
|
# Sequentially run all preprocessors on a request
|
109
127
|
# @param request [Landline::Request]
|
110
128
|
def run_preprocessors(request)
|
129
|
+
proccontext = get_context(request)
|
111
130
|
@preprocessors.each do |preproc|
|
112
|
-
|
131
|
+
proccontext.instance_exec(request, &preproc)
|
113
132
|
end
|
114
133
|
end
|
115
134
|
|
@@ -125,23 +144,27 @@ module Landline
|
|
125
144
|
# @return [Boolean] true if further navigation will be done
|
126
145
|
# @raise [UncaughtThrowError] by default throws :response if no matches found.
|
127
146
|
def process_wrapped(request)
|
128
|
-
@request = request
|
129
147
|
return false unless run_filters(request)
|
130
148
|
|
131
149
|
run_preprocessors(request)
|
132
150
|
enqueue_postprocessors(request)
|
133
151
|
@children.each do |x|
|
134
152
|
value = x.go(request)
|
135
|
-
return value if value
|
153
|
+
return exit_stack(request, value) if value
|
136
154
|
end
|
137
155
|
value = index(request)
|
138
|
-
return value if value
|
156
|
+
return exit_stack(request, value) if value
|
139
157
|
|
140
|
-
@bounce ?
|
158
|
+
@bounce ? exit_stack(request) : _die(404)
|
141
159
|
rescue StandardError => e
|
142
|
-
_die(500, backtrace: [e.to_s] + e.backtrace)
|
143
|
-
|
144
|
-
|
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
|
145
168
|
end
|
146
169
|
|
147
170
|
# Try to perform indexing on the path if possible
|
@@ -165,9 +188,10 @@ module Landline
|
|
165
188
|
# @param errorcode [Integer]
|
166
189
|
# @param backtrace [Array(String), nil]
|
167
190
|
# @raise [UncaughtThrowError] throws :finish to stop processing
|
168
|
-
def _die(errorcode, backtrace: nil)
|
191
|
+
def _die(request, errorcode, backtrace: nil)
|
192
|
+
proccontext = get_context(request)
|
169
193
|
throw :finish, [errorcode].append(
|
170
|
-
|
194
|
+
*proccontext.instance_exec(
|
171
195
|
errorcode,
|
172
196
|
backtrace: backtrace,
|
173
197
|
&(@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,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]
|
data/lib/landline/request.rb
CHANGED
@@ -21,10 +21,10 @@ module Landline
|
|
21
21
|
@param = {}
|
22
22
|
@splat = []
|
23
23
|
# Traversal route. Public and writable.
|
24
|
-
@path = URI.decode_www_form_component(env["PATH_INFO"]
|
24
|
+
@path = URI.decode_www_form_component(env["PATH_INFO"])
|
25
25
|
# File serving path. Public and writable.
|
26
26
|
@filepath = "/"
|
27
|
-
# Encapsulates all rack variables.
|
27
|
+
# Encapsulates all rack variables. Is no longer private, but usually should not be used directly
|
28
28
|
@rack = init_rack_vars(env)
|
29
29
|
# Internal navigation states. Private.
|
30
30
|
@states = []
|
@@ -35,18 +35,21 @@ module Landline
|
|
35
35
|
# Run postprocessors
|
36
36
|
# @param response [Landline::Response]
|
37
37
|
def run_postprocessors(response)
|
38
|
-
@postprocessors.
|
38
|
+
@postprocessors.reverse_each do |postproc|
|
39
39
|
postproc.call(self, response)
|
40
40
|
end
|
41
|
+
@postprocessors = []
|
41
42
|
end
|
42
43
|
|
43
44
|
# Returns request body (if POST data exists)
|
45
|
+
# @note reads data from rack.input, which is not rewindable. .body data is memoized.
|
44
46
|
# @return [nil, String]
|
45
47
|
def body
|
46
48
|
@body ||= @rack.input&.read
|
47
49
|
end
|
48
50
|
|
49
51
|
# Returns raw Rack input object
|
52
|
+
# @note Rack IO is not always rewindable - if it is read once, the data is gone (i.e. request.body will return nothing).
|
50
53
|
# @return [IO] (May not entirely be compatible with IO, see Rack/SPEC.rdoc)
|
51
54
|
def input
|
52
55
|
@rack.input
|
@@ -62,10 +65,33 @@ module Landline
|
|
62
65
|
@path, @param, @splat, @filepath = @states.pop
|
63
66
|
end
|
64
67
|
|
68
|
+
# Checks if response stream can be partially hijacked
|
69
|
+
def hijack?
|
70
|
+
@_original_env['rack.hijack?']
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns full hijack callback
|
74
|
+
def hijack
|
75
|
+
@_original_env['rack.hijack']
|
76
|
+
end
|
77
|
+
|
78
|
+
# Reconstructs rack env after modification
|
79
|
+
def env
|
80
|
+
path = @path
|
81
|
+
@_original_env.merge(reconstruct_headers)
|
82
|
+
.merge({
|
83
|
+
'PATH_INFO' => path,
|
84
|
+
'REQUEST_PATH' => path,
|
85
|
+
'QUERY_STRING' => query.query,
|
86
|
+
'REQUEST_URI' => "#{path}?#{query.query}"
|
87
|
+
})
|
88
|
+
.merge(reconstruct_cookie)
|
89
|
+
end
|
90
|
+
|
65
91
|
attr_reader :request_method, :script_name, :path_info, :server_name,
|
66
92
|
:server_port, :server_protocol, :headers, :param, :splat,
|
67
|
-
:postprocessors, :
|
68
|
-
attr_accessor :path, :filepath
|
93
|
+
:postprocessors, :cookies, :rack
|
94
|
+
attr_accessor :path, :filepath, :query
|
69
95
|
|
70
96
|
private
|
71
97
|
|
@@ -117,7 +143,7 @@ module Landline
|
|
117
143
|
.freeze
|
118
144
|
end
|
119
145
|
|
120
|
-
#
|
146
|
+
# Initialize headers hash
|
121
147
|
# @param env [Hash]
|
122
148
|
# @return Hash
|
123
149
|
def init_headers(env)
|
@@ -131,5 +157,30 @@ module Landline
|
|
131
157
|
x.downcase.gsub("_", "-") if x.is_a? String
|
132
158
|
end.freeze
|
133
159
|
end
|
160
|
+
|
161
|
+
# Reconstruct headers
|
162
|
+
def reconstruct_headers
|
163
|
+
@headers.filter_map do |k, v|
|
164
|
+
next unless v
|
165
|
+
|
166
|
+
if !['content-type', 'content-length',
|
167
|
+
'remote-addr'].include?(k) && (k.is_a? String)
|
168
|
+
k = "http_#{k}"
|
169
|
+
end
|
170
|
+
k = k.upcase.gsub("-", "_")
|
171
|
+
[k, v]
|
172
|
+
end.to_h
|
173
|
+
end
|
174
|
+
|
175
|
+
# Reconstruct cookie string
|
176
|
+
def reconstruct_cookie
|
177
|
+
return {} if @cookies.empty?
|
178
|
+
|
179
|
+
{
|
180
|
+
"HTTP_COOKIE" => @cookies.map do |_, v|
|
181
|
+
v.finalize_short
|
182
|
+
end.join(";")
|
183
|
+
}
|
184
|
+
end
|
134
185
|
end
|
135
186
|
end
|