landline 0.12.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6713c809dcb1f2216afff7f05d00e932c85d36c1b205359a2c8f8655b79fbb1
4
- data.tar.gz: 34846e521a08d55376a520a194417edf0cb66972bf6c052ac6fb5d888fea30f2
3
+ metadata.gz: 290ba72a21e71891ae33e361aea7afcff00d98429f96b303e061158c3bd34520
4
+ data.tar.gz: 545debbdab28e39eaa6f94e588bfcf371259cbcd6e33399517d7c3d3bd4393da
5
5
  SHA512:
6
- metadata.gz: b54438875d8532e95858567f0305b03baf00a6c88a735851116be73ea2b7619031226624ff846d05a7439050deff1b02acfe5c5a7d1a8afdb8e67b2ea6b520a1
7
- data.tar.gz: 03031f8d28c5e8657854cdff63e4f205d9b553cb03e34109b761a5f65207fd473005a61a5504a3f96517a37fe2d1f1b234b733916a3bd5f33f4de0e64a30fbab
6
+ metadata.gz: 3e0b7da7816dcfba10f2e3db537344b6057ca7377da5a6a9e3b8aa4892883d417339cce001935705ebf60816eb9a2965501de5adae111235e79c551b0ede6f5a
7
+ data.tar.gz: 2177431dd21d534a1936541f287647bb23ced5968bada8b7a642b44f5d21c16f4cc4690654a11ee6194d62dc7027511a819c69ab123ce81d562ee166cf20157f
data/README.md CHANGED
@@ -194,7 +194,7 @@ For things to render correctly, please install the `redcarpet` gem.
194
194
 
195
195
  ```plain
196
196
  Landline - an HTTP request pattern matching system
197
- Copyright (C) 2022 yessiest (yessiest@text.512mb.org)
197
+ Copyright (C) 2023-2024 yessiest (yessiest@text.512mb.org)
198
198
 
199
199
  This program is free software: you can redistribute it and/or modify
200
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
+ ```
@@ -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
- throw :finish, [errorcode].append(
14
- *(@origin.properties["handle.#{errorcode}"] or
15
- @origin.properties["handle.default"]).call(
16
- errorcode,
17
- backtrace: backtrace
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)
@@ -84,7 +84,7 @@ module Landline
84
84
  @session = Landline::Session::Session.new(
85
85
  request.cookies.dig('session', 0)&.value,
86
86
  proc do |value|
87
- delete_cookie("session", value)
87
+ delete_cookie("session")
88
88
  cookie("session", value)
89
89
  end
90
90
  )
@@ -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 [String, nil] nil returned if socket closes
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
- @data.deq
108
+ _process_events(proc { _read })
139
109
  end
140
110
 
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
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
- # Close the socket for writing
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 the socket
183
- # @return [void]
184
- def close
185
- _close
186
- @writable = false
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
- # 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
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.getc)
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
- # 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
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
@@ -37,6 +37,24 @@ module Landline
37
37
  context.instance_exec(&setup)
38
38
  end
39
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
+
40
58
  # Method callback on successful request navigation.
41
59
  # Finds the next appropriate path to go to.
42
60
  # @return [Boolean] true if further navigation will be done
@@ -138,8 +156,6 @@ module Landline
138
156
  return exit_stack(request, value) if value
139
157
 
140
158
  notfound(request)
141
- rescue StandardError => e
142
- _die(request, 500, backtrace: [e.to_s] + e.backtrace)
143
159
  end
144
160
 
145
161
  # Run enqueued postprocessors on navigation failure
@@ -175,16 +191,19 @@ module Landline
175
191
  # @param errorcode [Integer]
176
192
  # @param backtrace [Array(String), nil]
177
193
  # @raise [UncaughtThrowError] throws :finish to stop processing
178
- def _die(request, errorcode, backtrace: nil)
194
+ def _die(request, errorcode, backtrace: nil, error: nil)
179
195
  proccontext = get_context(request)
180
- throw :finish, [errorcode].append(
181
- *proccontext.instance_exec(
196
+ response = Landline::Response.convert(
197
+ proccontext.instance_exec(
182
198
  errorcode,
183
199
  backtrace: backtrace,
200
+ error: error,
184
201
  &(@properties["handle.#{errorcode}"] or
185
202
  @properties["handle.default"])
186
203
  )
187
204
  )
205
+ response.status = errorcode if response.status == 200
206
+ throw :finish, response
188
207
  end
189
208
  end
190
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 input.start_with?(@pattern)
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).start_with? @pattern
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
@@ -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] }
@@ -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)
@@ -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
@@ -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
- COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/
20
- COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o
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.12.1 "Concrete and Gold" (pre-alpha)'
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.12.1
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-05-07 00:00:00.000000000 Z
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
@@ -82,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
84
  - !ruby/object:Gem::Version
83
85
  version: '0'
84
86
  requirements: []
85
- rubygems_version: 3.5.6
87
+ rubygems_version: 3.5.16
86
88
  signing_key:
87
89
  specification_version: 4
88
90
  summary: Elegant HTTP DSL