landline 0.12.0 → 0.13.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 +3 -5
- data/STRUCTURE.md +129 -0
- data/lib/landline/app.rb +28 -13
- data/lib/landline/dsl/methods_common.rb +10 -7
- data/lib/landline/dsl/methods_probe.rb +3 -3
- data/lib/landline/extensions/session.rb +1 -1
- data/lib/landline/extensions/websocket.rb +80 -82
- data/lib/landline/path.rb +44 -38
- data/lib/landline/pattern_matching.rb +10 -2
- data/lib/landline/probe/handler.rb +10 -2
- data/lib/landline/probe.rb +0 -22
- data/lib/landline/request.rb +12 -2
- data/lib/landline/sandbox.rb +32 -0
- data/lib/landline/server.rb +3 -3
- data/lib/landline/util/cookie.rb +2 -0
- data/lib/landline/util/lookup.rb +3 -1
- data/lib/landline/util/multipart.rb +1 -1
- data/lib/landline/util/parseutils.rb +8 -4
- data/lib/landline.rb +2 -2
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 290ba72a21e71891ae33e361aea7afcff00d98429f96b303e061158c3bd34520
|
4
|
+
data.tar.gz: 545debbdab28e39eaa6f94e588bfcf371259cbcd6e33399517d7c3d3bd4393da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e0b7da7816dcfba10f2e3db537344b6057ca7377da5a6a9e3b8aa4892883d417339cce001935705ebf60816eb9a2965501de5adae111235e79c551b0ede6f5a
|
7
|
+
data.tar.gz: 2177431dd21d534a1936541f287647bb23ced5968bada8b7a642b44f5d21c16f4cc4690654a11ee6194d62dc7027511a819c69ab123ce81d562ee166cf20157f
|
data/README.md
CHANGED
@@ -139,10 +139,8 @@ end
|
|
139
139
|
class Application < Landline::App
|
140
140
|
use TimerMiddleware
|
141
141
|
|
142
|
-
|
143
|
-
|
144
|
-
"Hello world!"
|
145
|
-
end
|
142
|
+
get "/hello" do
|
143
|
+
"Hello world!"
|
146
144
|
end
|
147
145
|
end
|
148
146
|
|
@@ -196,7 +194,7 @@ For things to render correctly, please install the `redcarpet` gem.
|
|
196
194
|
|
197
195
|
```plain
|
198
196
|
Landline - an HTTP request pattern matching system
|
199
|
-
Copyright (C)
|
197
|
+
Copyright (C) 2023-2024 yessiest (yessiest@text.512mb.org)
|
200
198
|
|
201
199
|
This program is free software: you can redistribute it and/or modify
|
202
200
|
it under the terms of the GNU General Public License as published by
|
data/STRUCTURE.md
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# Landline library and application structure
|
2
|
+
|
3
|
+
## File layout
|
4
|
+
|
5
|
+
- `/landline/lib/*.rb` - core of the library
|
6
|
+
- `/landline/lib/dsl/` - module namespaces for use by executable contexts
|
7
|
+
- `/landline/lib/path/` - nodes that extend the Path class (traversable
|
8
|
+
node/setup execution context)
|
9
|
+
- `/landline/lib/probe/` - nodes that extend the Probe class (executable
|
10
|
+
nodes/per-request execution context)
|
11
|
+
- `/landline/lib/template/` - template engine adapters
|
12
|
+
- `/landline/lib/util/` - utility classes and modules
|
13
|
+
- `/landline/lib/pattern_matching/` - classes that implement path pattern
|
14
|
+
matching (i.e. globs, regex and static traversal paths)
|
15
|
+
|
16
|
+
## Architecture overview
|
17
|
+
|
18
|
+
The following chapter defines information that pertains to the overall
|
19
|
+
structure and interaction of landline's core abstractions - Nodes, Paths
|
20
|
+
and Probes.
|
21
|
+
|
22
|
+
For context: Node is an element of the path tree, and is another name for a
|
23
|
+
Probe (which is a leaf of the tree) or a path (which is an internal vertex
|
24
|
+
of the tree)
|
25
|
+
|
26
|
+
```plaintext
|
27
|
+
Path +-> Path +-> Probe
|
28
|
+
| |
|
29
|
+
| +-> Probe
|
30
|
+
+-> Probe
|
31
|
+
|
|
32
|
+
|-> Path -> Probe
|
33
|
+
```
|
34
|
+
|
35
|
+
### Execution contexts
|
36
|
+
|
37
|
+
In Sinatra, which this library takes most of its influence from, every
|
38
|
+
request creates its own instance of the Sinatra application. This seemed
|
39
|
+
like an excessive measure to solve a trivial problem of race conditions on a
|
40
|
+
shared execution context, so to solve that, Landline introduces per-request
|
41
|
+
execution contexts, which have a life time that spans the duration of
|
42
|
+
request processing.
|
43
|
+
|
44
|
+
In short, there are two distinct execution contexts:
|
45
|
+
|
46
|
+
- Per-request execution context, which is used by blocks that process the
|
47
|
+
request and which is bound to that given request
|
48
|
+
- Setup execution context, in which the Landline node tree is constructed
|
49
|
+
|
50
|
+
For every Path-like node, the supplied block is executed in the setup
|
51
|
+
context bound to that path.
|
52
|
+
|
53
|
+
For every Probe-like node, the supplied block is executed in the per-request
|
54
|
+
context of the request that is passed to the probe.
|
55
|
+
|
56
|
+
These execution contexts also affect the receivers of the instance variable
|
57
|
+
write and read operations. Additionally, the two types have different
|
58
|
+
sets of available methods, and every DSL method has a description attached
|
59
|
+
which describes in which context that method can be used (this is due to the
|
60
|
+
limitations of the YARD parser and due to the need of showing method
|
61
|
+
descriptions in the IDE)
|
62
|
+
|
63
|
+
### Request traversal
|
64
|
+
|
65
|
+
Requests in Landline traverse through a series of paths that determine
|
66
|
+
which route the request would take. All nodes are organized in a tree
|
67
|
+
structure, where the root of the tree is a Server object that handles
|
68
|
+
things likes localized jumps, request/response conversion and response
|
69
|
+
finalization, and root-level error/`die()` handling.
|
70
|
+
|
71
|
+
Every path-like node can be supplied with callbacks that perform
|
72
|
+
the following functions:
|
73
|
+
|
74
|
+
- filters - callbacks that filter incoming request objects
|
75
|
+
- preprocessors - callbacks that don't usually affect the routing decision
|
76
|
+
but can modify requests before they come through
|
77
|
+
- postprocessors - callbacks that execute either when a request exits a
|
78
|
+
given path after not finding any suitable probe-like node or when a
|
79
|
+
response to a request is being finalized
|
80
|
+
- pipeline - single callback that is injected in the execution stack that
|
81
|
+
can be used to catch throws and exceptions
|
82
|
+
|
83
|
+
(see `/examples/logging.ru`, `/examples/norxondor_gorgonax/config.ru`)
|
84
|
+
|
85
|
+
The execution contexts for every callback (except pipeline) are shared with
|
86
|
+
those of the probe-like execution contexts, meaning that an instance
|
87
|
+
variable written to in a filter or a preprocessor can be accessed from the
|
88
|
+
probe node or a postprocessor callback.
|
89
|
+
|
90
|
+
Once all preprocessors and filters are finished successfully, the request
|
91
|
+
is passed to all subsequent graph elements (either other Paths or Probes)
|
92
|
+
to check whether a given Node accepts that element.
|
93
|
+
|
94
|
+
If the subtree of the path contains at least one probe that accepts the
|
95
|
+
request, that probe finishes processing the request and either exits
|
96
|
+
the tree immediately by throwing the `:finish` signal with a response
|
97
|
+
or by returning a valid (not nil) response from the `process` method,
|
98
|
+
in which case the response ascends from the probe back to the root.
|
99
|
+
|
100
|
+
If none of the subsequent elements of a path accepted the request, the
|
101
|
+
path executes `die(404)` if `bounce` is not enabled, and if `bounce` is
|
102
|
+
enabled, the request exits from the path and proceeds to sibling
|
103
|
+
(adjacent) paths. (see `/examples/dirbounce.ru`)
|
104
|
+
|
105
|
+
A probe can finish the request by either throwing a response, by executing a
|
106
|
+
jump to another path, by explicitly dying with a status code or by throwing
|
107
|
+
an exception (which effectively causes `die(500)`)
|
108
|
+
(see `/examples/errorpages.ru`, `/examples/jumps.ru`)
|
109
|
+
|
110
|
+
If a jump is executed, the path of the request is rewritten locally, and the
|
111
|
+
request is fed through the server again for processing. This is effectively
|
112
|
+
a localized redirect which retains accumulated request processing
|
113
|
+
information (i.e. instance variables)
|
114
|
+
(see `/examples/jumps.ru`)
|
115
|
+
|
116
|
+
The tree can be changed at runtime, but it's generally not advisable to
|
117
|
+
depend on that fact. If, eventually, some solution will be found to
|
118
|
+
optimizing trees into linear routing paths, and if that solution is proven
|
119
|
+
to improve performance, this statement might become false.
|
120
|
+
|
121
|
+
```plaintext
|
122
|
+
+--------------------------------------+
|
123
|
+
v |
|
124
|
+
run (obj) -> App#call -> Server#call +-> Path#go -> ... -> Probe#go +
|
125
|
+
| | (if app acts as a wrapper for another
|
126
|
+
<-----------------------+ | Rack server and if response is 404
|
127
|
+
(returns back to Rack server | with x-cascade header set)
|
128
|
+
if no jumps occured) +-> passthrough#call
|
129
|
+
```
|
data/lib/landline/app.rb
CHANGED
@@ -3,18 +3,20 @@
|
|
3
3
|
module Landline
|
4
4
|
# Rack application interface
|
5
5
|
class App
|
6
|
-
# TODO: fix this mess somehow (probably impossible)
|
7
|
-
|
8
|
-
# @!parse include Landline::DSL::PathMethods
|
9
|
-
# @!parse include Landline::DSL::PathConstructors
|
10
|
-
# @!parse include Landline::DSL::ProbeConstructors
|
11
|
-
# @!parse include Landline::DSL::ProbeMethods
|
12
|
-
# @!parse include Landline::DSL::CommonMethods
|
13
6
|
class << self
|
7
|
+
# TODO: fix this mess somehow (probably impossible)
|
8
|
+
# @!parse include Landline::DSL::PathMethods
|
9
|
+
# @!parse include Landline::DSL::PathConstructors
|
10
|
+
# @!parse include Landline::DSL::ProbeConstructors
|
11
|
+
# @!parse include Landline::DSL::ProbeMethods
|
12
|
+
# @!parse include Landline::DSL::CommonMethods
|
13
|
+
|
14
14
|
# Duplicate used middleware for the subclassed app
|
15
15
|
def inherited(subclass)
|
16
16
|
super(subclass)
|
17
|
+
@setup_chain ||= []
|
17
18
|
subclass.middleware = @middleware.dup
|
19
|
+
subclass.setup_chain = @setup_chain.dup
|
18
20
|
end
|
19
21
|
|
20
22
|
# Include a middleware in application
|
@@ -24,17 +26,30 @@ module Landline
|
|
24
26
|
@middleware.append(middleware)
|
25
27
|
end
|
26
28
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
@setup_block = block
|
29
|
+
# Check if Server can respond to given symbol
|
30
|
+
def respond_to_missing?(symbol, _include_private)
|
31
|
+
Landline::ServerContext.instance_methods.include?(symbol) || super
|
31
32
|
end
|
32
33
|
|
33
|
-
|
34
|
+
# Store applied app manipulations
|
35
|
+
def method_missing(symbol, *args, **params, &callback)
|
36
|
+
if Landline::ServerContext.instance_methods.include? symbol
|
37
|
+
@setup_chain.append([symbol, args, params, callback])
|
38
|
+
else
|
39
|
+
super(symbol, *args, **params, &callback)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :middleware, :setup_chain
|
34
44
|
end
|
35
45
|
|
36
46
|
def initialize(*args, **opts)
|
37
|
-
|
47
|
+
setup_chain = self.class.setup_chain
|
48
|
+
@app = ::Landline::Server.new(*args, **opts) do
|
49
|
+
setup_chain.each do |symbol, cargs, cparams, callback|
|
50
|
+
send(symbol, *cargs, **cparams, &callback)
|
51
|
+
end
|
52
|
+
end
|
38
53
|
self.class.middleware&.reverse_each do |cls|
|
39
54
|
@app = cls.new(@app)
|
40
55
|
end
|
@@ -9,14 +9,17 @@ module Landline
|
|
9
9
|
# @param errorcode [Integer]
|
10
10
|
# @param backtrace [Array(String), nil]
|
11
11
|
# @raise [UncaughtThrowError] throws :finish to return back to Server
|
12
|
-
def die(errorcode, backtrace: nil)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
def die(errorcode, backtrace: nil, error: nil)
|
13
|
+
response = Landline::Response.convert(
|
14
|
+
(@origin.properties["handle.#{errorcode}"] or
|
15
|
+
@origin.properties["handle.default"]).call(
|
16
|
+
errorcode,
|
17
|
+
backtrace: backtrace,
|
18
|
+
error: error
|
19
|
+
)
|
19
20
|
)
|
21
|
+
response.status = errorcode if response.status == 200
|
22
|
+
throw :finish, response
|
20
23
|
end
|
21
24
|
|
22
25
|
# (in Landline::Probe context)
|
@@ -43,21 +43,21 @@ module Landline
|
|
43
43
|
# @param path [String]
|
44
44
|
def jump(path)
|
45
45
|
@origin.request.path = path
|
46
|
-
throw(:
|
46
|
+
throw(:finish, [307, { "x-internal-jump": true }, []])
|
47
47
|
end
|
48
48
|
|
49
49
|
# (in Landline::Probe context)
|
50
50
|
# Do clientside request redirection via 302 code
|
51
51
|
# @param path [String]
|
52
52
|
def redirect(path)
|
53
|
-
throw(:
|
53
|
+
throw(:finish, [302, { "location": path }, []])
|
54
54
|
end
|
55
55
|
|
56
56
|
# (in Landline::Probe context)
|
57
57
|
# Do clientside request redirection via 307 code
|
58
58
|
# @param path [String]
|
59
59
|
def redirect_with_method(path)
|
60
|
-
throw(:
|
60
|
+
throw(:finish, [307, { "location": path }, []])
|
61
61
|
end
|
62
62
|
|
63
63
|
alias code status
|
@@ -24,28 +24,6 @@ module Landline
|
|
24
24
|
@__listeners[event]&.delete(listener)
|
25
25
|
end
|
26
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
27
|
private
|
50
28
|
|
51
29
|
# Trigger the queue clearing process
|
@@ -95,19 +73,6 @@ module Landline
|
|
95
73
|
)
|
96
74
|
@readable = true
|
97
75
|
@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
76
|
end
|
112
77
|
|
113
78
|
# Send data through websocket
|
@@ -125,31 +90,33 @@ module Landline
|
|
125
90
|
type: type
|
126
91
|
)
|
127
92
|
@io.write(frame.to_s)
|
93
|
+
rescue Errno::EPIPE => e
|
94
|
+
@writable = false
|
95
|
+
_emit :error, e
|
96
|
+
close if @readable
|
97
|
+
nil
|
128
98
|
end
|
129
99
|
|
130
100
|
# Read data from socket synchronously
|
131
|
-
# @return [
|
101
|
+
# @return [WebSocket::Frame::Base, nil] nil if socket received a close event
|
132
102
|
def read
|
133
103
|
unless @readable
|
134
104
|
raise self.class::WebSocketError,
|
135
105
|
"socket closed for reading"
|
136
106
|
end
|
137
107
|
|
138
|
-
|
108
|
+
_process_events(proc { _read })
|
139
109
|
end
|
140
110
|
|
141
|
-
#
|
142
|
-
# @return [
|
143
|
-
def
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
111
|
+
# Read data from socket without blocking
|
112
|
+
# @return [WebSocket::Frame::Base, nil] nil if socket received a close event
|
113
|
+
def read_nonblock
|
114
|
+
unless @readable
|
115
|
+
raise self.class::WebSocketError,
|
116
|
+
"socket closed for reading"
|
117
|
+
end
|
148
118
|
|
149
|
-
|
150
|
-
def close_write
|
151
|
-
@writable = false
|
152
|
-
@io.close_write
|
119
|
+
_process_events(proc { _read_nonblock })
|
153
120
|
end
|
154
121
|
|
155
122
|
# Establish a connection through handshake
|
@@ -179,58 +146,88 @@ module Landline
|
|
179
146
|
handshake
|
180
147
|
end
|
181
148
|
|
182
|
-
# Close
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
149
|
+
# Close socket for reading
|
150
|
+
def close_read
|
151
|
+
raise WebSocketError, 'socket closed for reading' unless @readable
|
152
|
+
|
153
|
+
_emit :close
|
187
154
|
@readable = false
|
155
|
+
@io.close_read
|
156
|
+
end
|
157
|
+
|
158
|
+
# Close socket for reading
|
159
|
+
def close_write
|
160
|
+
raise WebSocketError, 'socket closed for writing' unless @writable
|
161
|
+
|
162
|
+
write(nil, type: :close)
|
163
|
+
@writable = false
|
164
|
+
@io.close_write
|
165
|
+
end
|
166
|
+
|
167
|
+
# Close the socket entirely
|
168
|
+
def close
|
169
|
+
raise WebSocketError, 'socket closed' unless @writable or @readable
|
170
|
+
|
171
|
+
close_read if @readable
|
172
|
+
close_write if @writable
|
188
173
|
end
|
189
174
|
|
175
|
+
# Obtain internal IO object
|
176
|
+
# @return [IO]
|
177
|
+
def to_io
|
178
|
+
io
|
179
|
+
end
|
180
|
+
|
181
|
+
attr_reader :io, :readable, :writable
|
182
|
+
|
190
183
|
private
|
191
184
|
|
192
|
-
#
|
193
|
-
# @
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
185
|
+
# Process incoming websocket events
|
186
|
+
# @param next_frame [#call] callback to get the next frame
|
187
|
+
# @return [WebSocket::Frame::Base, nil]
|
188
|
+
def _process_events(next_frame)
|
189
|
+
loop do
|
190
|
+
frame = next_frame.call
|
191
|
+
return nil unless frame
|
192
|
+
|
193
|
+
case frame.type
|
194
|
+
when :binary, :text, :pong then return frame
|
195
|
+
when :ping
|
196
|
+
write frame.to_s, type: :pong
|
197
|
+
when :close
|
198
|
+
close_read
|
199
|
+
return nil
|
200
|
+
else raise WebSocketError, "unknown frame type #{frame.type}"
|
204
201
|
end
|
205
|
-
rescue IOError => e
|
206
|
-
@writable = false
|
207
|
-
_emit :error, e
|
208
|
-
close
|
209
|
-
ensure
|
210
|
-
close_read
|
211
202
|
end
|
212
203
|
end
|
213
204
|
|
214
205
|
# Receive data through websocket
|
215
206
|
# @return [String] output from frame
|
216
207
|
def _read
|
217
|
-
while (char = @io.
|
208
|
+
while (char = @io.read(1))
|
218
209
|
@frame_parser << char
|
219
210
|
frame = @frame_parser.next
|
220
211
|
return frame if frame
|
221
212
|
end
|
213
|
+
rescue Errno::ECONNRESET => e
|
214
|
+
_emit :error, e
|
215
|
+
close if @readable or @writable
|
216
|
+
nil
|
222
217
|
end
|
223
218
|
|
224
|
-
#
|
225
|
-
# @return [
|
226
|
-
def
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
219
|
+
# Receive data through websocket asynchronously
|
220
|
+
# @return [String] output from frame
|
221
|
+
def _read_nonblock
|
222
|
+
while (char = @io.read_nonblock(1))
|
223
|
+
@frame_parser << char
|
224
|
+
frame = @frame_parser.next
|
225
|
+
return frame if frame
|
226
|
+
end
|
227
|
+
rescue Errno::ECONNRESET => e
|
228
|
+
_emit :error, e
|
229
|
+
close if @readable or @writable
|
230
|
+
nil
|
234
231
|
end
|
235
232
|
end
|
236
233
|
end
|
@@ -267,6 +264,7 @@ module Landline
|
|
267
264
|
**@params
|
268
265
|
)
|
269
266
|
)
|
267
|
+
""
|
270
268
|
end
|
271
269
|
end
|
272
270
|
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
|
@@ -31,21 +17,8 @@ module Landline
|
|
31
17
|
end
|
32
18
|
end
|
33
19
|
|
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
|
-
|
45
20
|
# Primary building block of request navigation.
|
46
21
|
class Path < Landline::Node
|
47
|
-
ExecutionOrigin = Landline::PathExecutionOrigin
|
48
|
-
ProcContext = Landline::ProcessorContext
|
49
22
|
Context = Landline::PathContext
|
50
23
|
|
51
24
|
# @param path [Object] Object to generate {Landline::Pattern} from
|
@@ -64,6 +37,24 @@ module Landline
|
|
64
37
|
context.instance_exec(&setup)
|
65
38
|
end
|
66
39
|
|
40
|
+
# (see ::Landline::Node#go)
|
41
|
+
def go(request)
|
42
|
+
# This is done to allow pipeline to interject handlers
|
43
|
+
# I'm more than willing to admit that this is stupid,
|
44
|
+
# but it is well worth the logical flexibility.
|
45
|
+
if ['handle.default', 'handle.505'].any? do |x|
|
46
|
+
@properties.storage.include? x
|
47
|
+
end
|
48
|
+
begin
|
49
|
+
super(request)
|
50
|
+
rescue StandardError => e
|
51
|
+
_die(request, 500, backtrace: [e.to_s] + e.backtrace, error: e)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
super(request)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
67
58
|
# Method callback on successful request navigation.
|
68
59
|
# Finds the next appropriate path to go to.
|
69
60
|
# @return [Boolean] true if further navigation will be done
|
@@ -108,8 +99,8 @@ module Landline
|
|
108
99
|
|
109
100
|
# Create an execution context for in-path processing blocks
|
110
101
|
def get_context(request)
|
111
|
-
|
112
|
-
|
102
|
+
request.context.origin.properties.lookup = @properties
|
103
|
+
request.context
|
113
104
|
end
|
114
105
|
|
115
106
|
# Sequentially run through all filters and drop request if one is false
|
@@ -118,7 +109,10 @@ module Landline
|
|
118
109
|
def run_filters(request)
|
119
110
|
proccontext = get_context(request)
|
120
111
|
@filters.each do |filter|
|
121
|
-
|
112
|
+
output = catch(:break) do
|
113
|
+
proccontext.instance_exec(request, &filter)
|
114
|
+
end
|
115
|
+
return false unless output
|
122
116
|
end
|
123
117
|
true
|
124
118
|
end
|
@@ -128,8 +122,13 @@ module Landline
|
|
128
122
|
def run_preprocessors(request)
|
129
123
|
proccontext = get_context(request)
|
130
124
|
@preprocessors.each do |preproc|
|
131
|
-
|
125
|
+
output = catch(:break) do
|
126
|
+
proccontext.instance_exec(request, &preproc)
|
127
|
+
true
|
128
|
+
end
|
129
|
+
return false unless output
|
132
130
|
end
|
131
|
+
true
|
133
132
|
end
|
134
133
|
|
135
134
|
# Append postprocessors to request
|
@@ -146,7 +145,8 @@ module Landline
|
|
146
145
|
def process_wrapped(request)
|
147
146
|
return false unless run_filters(request)
|
148
147
|
|
149
|
-
run_preprocessors(request)
|
148
|
+
return false unless run_preprocessors(request)
|
149
|
+
|
150
150
|
enqueue_postprocessors(request)
|
151
151
|
@children.each do |x|
|
152
152
|
value = x.go(request)
|
@@ -155,9 +155,7 @@ module Landline
|
|
155
155
|
value = index(request)
|
156
156
|
return exit_stack(request, value) if value
|
157
157
|
|
158
|
-
|
159
|
-
rescue StandardError => e
|
160
|
-
_die(request, 500, backtrace: [e.to_s] + e.backtrace)
|
158
|
+
notfound(request)
|
161
159
|
end
|
162
160
|
|
163
161
|
# Run enqueued postprocessors on navigation failure
|
@@ -167,6 +165,11 @@ module Landline
|
|
167
165
|
response
|
168
166
|
end
|
169
167
|
|
168
|
+
# Exit with failure or throw 404
|
169
|
+
def notfound(request)
|
170
|
+
@bounce ? exit_stack(request) : _die(request, 404)
|
171
|
+
end
|
172
|
+
|
170
173
|
# Try to perform indexing on the path if possible
|
171
174
|
# @param request [Landline::Request]
|
172
175
|
# @return [Boolean] true if indexing succeeded
|
@@ -188,16 +191,19 @@ module Landline
|
|
188
191
|
# @param errorcode [Integer]
|
189
192
|
# @param backtrace [Array(String), nil]
|
190
193
|
# @raise [UncaughtThrowError] throws :finish to stop processing
|
191
|
-
def _die(request, errorcode, backtrace: nil)
|
194
|
+
def _die(request, errorcode, backtrace: nil, error: nil)
|
192
195
|
proccontext = get_context(request)
|
193
|
-
|
194
|
-
|
196
|
+
response = Landline::Response.convert(
|
197
|
+
proccontext.instance_exec(
|
195
198
|
errorcode,
|
196
199
|
backtrace: backtrace,
|
200
|
+
error: error,
|
197
201
|
&(@properties["handle.#{errorcode}"] or
|
198
202
|
@properties["handle.default"])
|
199
203
|
)
|
200
204
|
)
|
205
|
+
response.status = errorcode if response.status == 200
|
206
|
+
throw :finish, response
|
201
207
|
end
|
202
208
|
end
|
203
209
|
end
|
@@ -35,7 +35,7 @@ module Landline
|
|
35
35
|
def match(input)
|
36
36
|
if @pattern.is_a? String
|
37
37
|
input = Landline::PatternMatching.canonicalize(input)
|
38
|
-
if
|
38
|
+
if _match?(input)
|
39
39
|
[input.delete_prefix(@pattern), [], {}]
|
40
40
|
else
|
41
41
|
false
|
@@ -51,7 +51,8 @@ module Landline
|
|
51
51
|
# @return [Boolean]
|
52
52
|
def match?(input)
|
53
53
|
if @pattern.is_a? String
|
54
|
-
Landline::PatternMatching.canonicalize(input)
|
54
|
+
input = Landline::PatternMatching.canonicalize(input)
|
55
|
+
_match?(input)
|
55
56
|
else
|
56
57
|
@pattern.match?(input)
|
57
58
|
end
|
@@ -59,6 +60,13 @@ module Landline
|
|
59
60
|
|
60
61
|
private
|
61
62
|
|
63
|
+
def _match?(input)
|
64
|
+
parts = input.split("/")
|
65
|
+
@pattern.split("/").map.with_index do |part, index|
|
66
|
+
parts[index] == part
|
67
|
+
end.all?(true)
|
68
|
+
end
|
69
|
+
|
62
70
|
def patternify(pattern)
|
63
71
|
classdomain = Landline::PatternMatching
|
64
72
|
classdomain.constants
|
@@ -29,8 +29,8 @@ module Landline
|
|
29
29
|
# @return [Boolean] true if further navigation is possible
|
30
30
|
# @raise [UncaughtThrowError] may raise if die() is called.
|
31
31
|
def process(request)
|
32
|
-
origin =
|
33
|
-
|
32
|
+
origin, context = get_context(request)
|
33
|
+
|
34
34
|
return reject(request) unless request.path.match?(/^\/?$/)
|
35
35
|
|
36
36
|
response = catch(:break) do
|
@@ -47,6 +47,14 @@ module Landline
|
|
47
47
|
end
|
48
48
|
throw :finish, response
|
49
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
|
50
58
|
end
|
51
59
|
end
|
52
60
|
end
|
data/lib/landline/probe.rb
CHANGED
@@ -24,28 +24,6 @@ module Landline
|
|
24
24
|
autoload :Link, "landline/probe/crosscall_handler"
|
25
25
|
end
|
26
26
|
|
27
|
-
# Context that provides execution context for Probes.
|
28
|
-
class ProbeContext
|
29
|
-
include Landline::DSL::ProbeConstructors
|
30
|
-
include Landline::DSL::ProbeMethods
|
31
|
-
include Landline::DSL::CommonMethods
|
32
|
-
|
33
|
-
def initialize(origin)
|
34
|
-
@origin = origin
|
35
|
-
end
|
36
|
-
end
|
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
|
-
|
49
27
|
# Test probe. Also base for all "reactive" nodes.
|
50
28
|
class Probe < Landline::Node
|
51
29
|
# @param path [Object]
|
data/lib/landline/request.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require_relative 'util/query'
|
5
5
|
require_relative 'util/cookie'
|
6
|
+
require_relative 'sandbox'
|
6
7
|
|
7
8
|
module Landline
|
8
9
|
# Request wrapper for Rack protocol
|
@@ -30,13 +31,16 @@ module Landline
|
|
30
31
|
@states = []
|
31
32
|
# Postprocessors for current request
|
32
33
|
@postprocessors = []
|
34
|
+
# Execution context
|
35
|
+
@context = init_context
|
33
36
|
end
|
34
37
|
|
35
38
|
# Run postprocessors
|
36
39
|
# @param response [Landline::Response]
|
37
40
|
def run_postprocessors(response)
|
41
|
+
@context.origin.properties.lookup = {}
|
38
42
|
@postprocessors.reverse_each do |postproc|
|
39
|
-
|
43
|
+
@context.instance_exec(self, response, &postproc)
|
40
44
|
end
|
41
45
|
@postprocessors = []
|
42
46
|
end
|
@@ -90,11 +94,17 @@ module Landline
|
|
90
94
|
|
91
95
|
attr_reader :request_method, :script_name, :path_info, :server_name,
|
92
96
|
:server_port, :server_protocol, :headers, :param, :splat,
|
93
|
-
:postprocessors, :cookies, :rack
|
97
|
+
:postprocessors, :cookies, :rack, :context
|
94
98
|
attr_accessor :path, :filepath, :query
|
95
99
|
|
96
100
|
private
|
97
101
|
|
102
|
+
# Initialize execution context
|
103
|
+
def init_context
|
104
|
+
origin = Landline::ProcessorOrigin.new(self, {})
|
105
|
+
Landline::ProcessorContext.new(origin)
|
106
|
+
end
|
107
|
+
|
98
108
|
# Initialize basic rack request parameters
|
99
109
|
# @param env [Hash]
|
100
110
|
def init_request_params(env)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'dsl/methods_common'
|
4
|
+
require_relative 'dsl/methods_probe'
|
5
|
+
require_relative 'dsl/constructors_probe'
|
6
|
+
require_relative 'util/lookup'
|
7
|
+
|
8
|
+
module Landline
|
9
|
+
# Execution context for filters and preprocessors.
|
10
|
+
class ProcessorContext
|
11
|
+
include Landline::DSL::CommonMethods
|
12
|
+
include Landline::DSL::ProbeMethods
|
13
|
+
include Landline::DSL::ProbeConstructors
|
14
|
+
|
15
|
+
def initialize(path)
|
16
|
+
@origin = path
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :origin
|
20
|
+
end
|
21
|
+
|
22
|
+
# Ephemeral proxy class to which callback execution binds
|
23
|
+
class ProcessorOrigin
|
24
|
+
def initialize(request, properties)
|
25
|
+
@request = request
|
26
|
+
@properties = Landline::Util::LookupROProxy.new(properties)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :response
|
30
|
+
attr_reader :request, :properties
|
31
|
+
end
|
32
|
+
end
|
data/lib/landline/server.rb
CHANGED
@@ -14,7 +14,7 @@ module Landline
|
|
14
14
|
# @param parent [Landline::Node, nil] Parent object to inherit properties to
|
15
15
|
# @param setup [#call] Setup block
|
16
16
|
def initialize(passthrough = nil, parent: nil, **opts, &setup)
|
17
|
-
super("", parent:
|
17
|
+
super("", parent: parent, **opts, &setup)
|
18
18
|
return if parent
|
19
19
|
|
20
20
|
@passthrough = passthrough
|
@@ -58,14 +58,14 @@ module Landline
|
|
58
58
|
def setup_properties(*_args, **_opts)
|
59
59
|
{
|
60
60
|
"index" => [],
|
61
|
-
"handle.default" => proc do |code, backtrace: nil|
|
61
|
+
"handle.default" => proc do |code, backtrace: nil, **_extra|
|
62
62
|
page = Landline::Util.default_error_page(code, backtrace)
|
63
63
|
headers = {
|
64
64
|
"content-length": page.bytesize,
|
65
65
|
"content-type": "text/html",
|
66
66
|
"x-cascade": true
|
67
67
|
}
|
68
|
-
[headers, page]
|
68
|
+
[code, headers, page]
|
69
69
|
end,
|
70
70
|
"path" => "/"
|
71
71
|
}.each { |k, v| @properties[k] = v unless @properties[k] }
|
data/lib/landline/util/cookie.rb
CHANGED
@@ -115,6 +115,8 @@ module Landline
|
|
115
115
|
|
116
116
|
data.split(";").map do |cookiestr|
|
117
117
|
key, value = cookiestr.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
|
118
|
+
next unless key and value
|
119
|
+
|
118
120
|
cookie = Cookie.new(key, value)
|
119
121
|
if hash[cookie.key]
|
120
122
|
hash[cookie.key].append(cookie)
|
data/lib/landline/util/lookup.rb
CHANGED
@@ -31,7 +31,7 @@ module Landline
|
|
31
31
|
@storage[key] = value
|
32
32
|
end
|
33
33
|
|
34
|
-
attr_accessor :parent
|
34
|
+
attr_accessor :parent, :storage
|
35
35
|
end
|
36
36
|
|
37
37
|
# Read-only lookup proxy
|
@@ -46,6 +46,8 @@ module Landline
|
|
46
46
|
def [](key)
|
47
47
|
@lookup.[](key)
|
48
48
|
end
|
49
|
+
|
50
|
+
attr_accessor :lookup
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
@@ -28,7 +28,7 @@ module Landline
|
|
28
28
|
# Decode charset parameter
|
29
29
|
def decode(data)
|
30
30
|
data = Landline::Util.unescape_html(data)
|
31
|
-
return data unless self.headers['charset']
|
31
|
+
return data.force_encoding("UTF-8") unless self.headers['charset']
|
32
32
|
|
33
33
|
data.force_encoding(self.headers['charset']).encode("UTF-8")
|
34
34
|
end
|
@@ -15,12 +15,16 @@ module Landline
|
|
15
15
|
PRINTCHAR = /[\x2-\x7E]/
|
16
16
|
# Matches 1 or more CHARs excluding CTLs
|
17
17
|
PRINTABLE = /#{PRINTCHAR}+/o
|
18
|
+
# !! RFC 6265 IS PROPOSED AND NOT AN IMPLEMENTED STANDARD YET !!
|
18
19
|
# Matches the RFC6265 definition of a cookie-octet
|
19
|
-
|
20
|
-
|
21
|
-
COOKIE_NAME = TOKEN
|
20
|
+
# COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o
|
21
|
+
# COOKIE_NAME = TOKEN
|
22
22
|
# Matches the RFC6265 definition of cookie-pair.
|
23
23
|
# Captures name (1) and value (2).
|
24
|
+
# !! RFC 6265 IS PROPOSED AND NOT AN IMPLEMENTED STANDARD YET !!
|
25
|
+
COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/
|
26
|
+
COOKIE_NAME = /[^;,=\s]*/
|
27
|
+
COOKIE_VALUE = /[^;,\s]*/
|
24
28
|
COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\z/o
|
25
29
|
# Matches a very abstract definition of a quoted header paramter.
|
26
30
|
# Captures name (1) and value (2).
|
@@ -83,7 +87,7 @@ module Landline
|
|
83
87
|
unless input.match? HeaderRegexp::PRINTABLE
|
84
88
|
raise Landline::ParsingError, "input is not ascii printable"
|
85
89
|
end
|
86
|
-
|
90
|
+
|
87
91
|
opts.each do |key, value|
|
88
92
|
check_param(key, value)
|
89
93
|
newparam = if [String, Integer].include? value.class
|
data/lib/landline.rb
CHANGED
@@ -12,11 +12,11 @@ require_relative 'landline/app'
|
|
12
12
|
# Landline is a backend framework born as a by-product of experimentation
|
13
13
|
module Landline
|
14
14
|
# Landline version
|
15
|
-
VERSION = '0.
|
15
|
+
VERSION = '0.13.0 "Realign" (pre-alpha)'
|
16
16
|
|
17
17
|
# Landline branding and version
|
18
18
|
VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n".freeze
|
19
19
|
|
20
20
|
# Landline copyright
|
21
|
-
COPYRIGHT = "Copyright 2023 Yessiest"
|
21
|
+
COPYRIGHT = "Copyright 2023-2024 Yessiest"
|
22
22
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: landline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yessiest
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun.
|
@@ -21,10 +21,12 @@ extra_rdoc_files:
|
|
21
21
|
- HACKING.md
|
22
22
|
- LICENSE.md
|
23
23
|
- README.md
|
24
|
+
- STRUCTURE.md
|
24
25
|
files:
|
25
26
|
- HACKING.md
|
26
27
|
- LICENSE.md
|
27
28
|
- README.md
|
29
|
+
- STRUCTURE.md
|
28
30
|
- lib/landline.rb
|
29
31
|
- lib/landline/app.rb
|
30
32
|
- lib/landline/dsl/constructors_path.rb
|
@@ -48,6 +50,7 @@ files:
|
|
48
50
|
- lib/landline/probe/serve_handler.rb
|
49
51
|
- lib/landline/request.rb
|
50
52
|
- lib/landline/response.rb
|
53
|
+
- lib/landline/sandbox.rb
|
51
54
|
- lib/landline/server.rb
|
52
55
|
- lib/landline/template.rb
|
53
56
|
- lib/landline/template/erb.rb
|
@@ -81,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
84
|
- !ruby/object:Gem::Version
|
82
85
|
version: '0'
|
83
86
|
requirements: []
|
84
|
-
rubygems_version: 3.5.
|
87
|
+
rubygems_version: 3.5.16
|
85
88
|
signing_key:
|
86
89
|
specification_version: 4
|
87
90
|
summary: Elegant HTTP DSL
|