landline 0.9.2

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/HACKING.md +30 -0
  3. data/LAYOUT.md +59 -0
  4. data/LICENSE.md +660 -0
  5. data/README.md +159 -0
  6. data/lib/landline/dsl/constructors_path.rb +107 -0
  7. data/lib/landline/dsl/constructors_probe.rb +28 -0
  8. data/lib/landline/dsl/methods_common.rb +28 -0
  9. data/lib/landline/dsl/methods_path.rb +75 -0
  10. data/lib/landline/dsl/methods_probe.rb +129 -0
  11. data/lib/landline/dsl/methods_template.rb +16 -0
  12. data/lib/landline/node.rb +87 -0
  13. data/lib/landline/path.rb +157 -0
  14. data/lib/landline/pattern_matching/glob.rb +168 -0
  15. data/lib/landline/pattern_matching/rematch.rb +49 -0
  16. data/lib/landline/pattern_matching/util.rb +15 -0
  17. data/lib/landline/pattern_matching.rb +75 -0
  18. data/lib/landline/probe/handler.rb +56 -0
  19. data/lib/landline/probe/http_method.rb +74 -0
  20. data/lib/landline/probe/serve_handler.rb +39 -0
  21. data/lib/landline/probe.rb +62 -0
  22. data/lib/landline/request.rb +135 -0
  23. data/lib/landline/response.rb +140 -0
  24. data/lib/landline/server.rb +49 -0
  25. data/lib/landline/template/erb.rb +27 -0
  26. data/lib/landline/template/erubi.rb +36 -0
  27. data/lib/landline/template.rb +95 -0
  28. data/lib/landline/util/cookie.rb +150 -0
  29. data/lib/landline/util/errors.rb +11 -0
  30. data/lib/landline/util/html.rb +119 -0
  31. data/lib/landline/util/lookup.rb +37 -0
  32. data/lib/landline/util/mime.rb +1276 -0
  33. data/lib/landline/util/multipart.rb +175 -0
  34. data/lib/landline/util/parsesorting.rb +37 -0
  35. data/lib/landline/util/parseutils.rb +111 -0
  36. data/lib/landline/util/query.rb +66 -0
  37. data/lib/landline.rb +20 -0
  38. metadata +85 -0
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Landline - an HTTP DSL
2
+
3
+ Landline is a library that provides a minimalistic DSL for creating
4
+ web services. It doesn't include patterns, middleware, or anything that
5
+ could be considered application logic. It does a few things, and hopefully
6
+ it does them well:
7
+
8
+ - Routing HTTP requests to handlers
9
+ - Processing HTTP requests (cookies, headers, etc.)
10
+ - Filtering, preprocessing and postprocessing requests
11
+ - Creating responses from templates using various template engines
12
+ - Parsing and handling forms and queries
13
+ - Connecting multiple Landline applications together
14
+
15
+ As such, the library is pretty thin and can be used to build more complex
16
+ applications.
17
+
18
+ As of now it is using Rack as the webserver adapter, but ideally it
19
+ shouldn't take much work to make it run on top of any webserver.
20
+
21
+ Landline was made mostly for fun. Ideally it will become something more,
22
+ but as of yet it's just an experiment revolving around Ruby Metaprogramming
23
+ and its DSL capabilities.
24
+
25
+ ## Examples
26
+
27
+ A simple "Hello, World!" app using Landline
28
+
29
+ ```ruby
30
+ require 'landline'
31
+
32
+ app = Landline::Server.new do
33
+ get "/hello" do
34
+ header "content-type", "text/plain"
35
+ "Hello world!"
36
+ end
37
+ end
38
+
39
+ run app
40
+ ```
41
+
42
+ A push/pull stack as an app
43
+
44
+ ```ruby
45
+ require 'landline'
46
+
47
+ stack = []
48
+
49
+ app = Landline::Server.new do
50
+ get "/pop" do
51
+ header 'content-type', 'text/plain'
52
+ stack.pop.to_s
53
+ end
54
+ post "/push" do
55
+ header 'content-type', 'text/plain'
56
+ stack.push(request.body)
57
+ request.body
58
+ end
59
+ end
60
+
61
+ run app
62
+ ```
63
+
64
+ Several push/pull buckets
65
+
66
+ ```ruby
67
+ require 'landline'
68
+
69
+ stack = { "1" => [], "2" => [], "3" => [] }
70
+
71
+ app = Landline::Server.new do
72
+ path "bucket_(1|2|3)" do
73
+ get "pop" do |bucket|
74
+ header "content-type", "text/plain"
75
+ stack[bucket].pop.to_s
76
+ end
77
+ post "push" do |bucket|
78
+ header "content-type", "text/plain"
79
+ stack[bucket].push(request.body)
80
+ request.body
81
+ end
82
+ end
83
+ end
84
+
85
+ run app
86
+ ```
87
+
88
+ Static file serving
89
+ (Note: index applies *only* to /var/www (to the path its defined in))
90
+
91
+ ```ruby
92
+ require 'landline'
93
+
94
+ app = Landline::Server.new do
95
+ root "/var/www"
96
+ index ["index.html","index.htm"]
97
+ serve "**/*.(html|htm)"
98
+ end
99
+
100
+ run app
101
+ ```
102
+
103
+ Logging on a particular path
104
+
105
+ ```ruby
106
+ require 'landline'
107
+
108
+ app = Landline::Server.new do
109
+ path "unimportant" do
110
+ get "version" do
111
+ header "content-type", "text/plain"
112
+ "1337 (the best one)"
113
+ end
114
+ end
115
+ path "important" do
116
+ preprocess do |req|
117
+ # Implement logging logic here
118
+ puts "Client at #{req.headers['remote-addr']} wanted to access something /important!"
119
+ end
120
+ get "answer" do
121
+ header "content-type", "application/json"
122
+ '{"answer":42, "desc":"something important!"}'
123
+ end
124
+ end
125
+ end
126
+
127
+ run app
128
+ ```
129
+
130
+ And a lot more to be found in /examples in this repo.
131
+
132
+ ## Name
133
+
134
+ The name is, quite literally, a metaphor for request routing.
135
+
136
+ ## Documentation
137
+
138
+ Documentation can be generated using `yard doc`.
139
+ For things to render correctly, please install the `redcarpet` gem.
140
+
141
+ ## License
142
+
143
+ ```plain
144
+ Landline - an HTTP request pattern matching system
145
+ Copyright (C) 2022 yessiest (yessiest@memeware.net)
146
+
147
+ This program is free software: you can redistribute it and/or modify
148
+ it under the terms of the GNU General Public License as published by
149
+ the Free Software Foundation, either version 3 of the License, or
150
+ (at your option) any later version.
151
+
152
+ This program is distributed in the hope that it will be useful,
153
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
154
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
155
+ GNU General Public License for more details.
156
+
157
+ You should have received a copy of the GNU General Public License
158
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
159
+ ```
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landline
4
+ # Shared DSL methods
5
+ module DSL
6
+ # Path (and subclasses) DSL constructors
7
+ module PathConstructors
8
+ # Append a Node child object to the list of children
9
+ def register(obj)
10
+ unless obj.is_a? Landline::Node
11
+ raise ArgumentError, "register accepts node children only"
12
+ end
13
+
14
+ @origin.children.append(obj)
15
+ end
16
+
17
+ # Create a new {Landline::Path} object
18
+ def path(path, **args, &setup)
19
+ register(Landline::Path.new(path, parent: @origin, **args, &setup))
20
+ end
21
+
22
+ # Create a new {Landline::Handlers::Probe} object
23
+ def probe(path, **args, &_setup)
24
+ register(Landline::Handlers::Probe.new(path,
25
+ parent: @origin,
26
+ **args))
27
+ end
28
+
29
+ # Create a new {Landline::Handlers::GETHandler} object
30
+ def get(path, **args, &setup)
31
+ register(Landline::Handlers::GET.new(path,
32
+ parent: @origin,
33
+ **args,
34
+ &setup))
35
+ end
36
+
37
+ # create a new {Landline::Handlers::POSTHandler} object
38
+ def post(path, **args, &setup)
39
+ register(Landline::Handlers::POST.new(path,
40
+ parent: @origin,
41
+ **args,
42
+ &setup))
43
+ end
44
+
45
+ # Create a new {Landline::Handlers::PUTHandler} object
46
+ def put(path, **args, &setup)
47
+ register(Landline::Handlers::PUT.new(path,
48
+ parent: @origin,
49
+ **args,
50
+ &setup))
51
+ end
52
+
53
+ # Create a new {Landline::Handlers::HEADHandler} object
54
+ def head(path, **args, &setup)
55
+ register(Landline::Handlers::HEAD.new(path,
56
+ parent: @origin,
57
+ **args,
58
+ &setup))
59
+ end
60
+
61
+ # Create a new {Landline::Handlers::DELETEHandler} object
62
+ def delete(path, **args, &setup)
63
+ register(Landline::Handlers::DELETE.new(path,
64
+ parent: @origin,
65
+ **args,
66
+ &setup))
67
+ end
68
+
69
+ # Create a new {Landline::Handlers::CONNECTHandler} object
70
+ def connect(path, **args, &setup)
71
+ register(Landline::Handlers::CONNECT.new(path,
72
+ parent: @origin,
73
+ **args,
74
+ &setup))
75
+ end
76
+
77
+ # Create a new {Landline::Handlers::TRACEHandler} object
78
+ def trace(path, **args, &setup)
79
+ register(Landline::Handlers::TRACE.new(path,
80
+ parent: @origin,
81
+ **args,
82
+ &setup))
83
+ end
84
+
85
+ # Create a new {Landline::Handlers::PATCHHandler} object
86
+ def patch(path, **args, &setup)
87
+ register(Landline::Handlers::PATCH.new(path,
88
+ parent: @origin,
89
+ **args,
90
+ &setup))
91
+ end
92
+
93
+ # Create a new {Landline::Handlers::OPTIONSHandler} object
94
+ def options(path, **args, &setup)
95
+ register(Landline::Handlers::OPTIONS.new(path,
96
+ parent: @origin,
97
+ **args,
98
+ &setup))
99
+ end
100
+
101
+ # Create a new {Landline::Handlers::GETHandler} that serves static files
102
+ def serve(path)
103
+ register(Landline::Handlers::Serve.new(path, parent: @origin))
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landline
4
+ module DSL
5
+ # Probe (and subclasses) DSL construct
6
+ module ProbeConstructors
7
+ # Create a new erb template
8
+ # @see {Landline::Template#new}
9
+ def erb(input, vars = {})
10
+ Landline::Templates::ERB.new(input,
11
+ vars,
12
+ parent: @origin)
13
+ end
14
+
15
+ # Create a new erb template using Erubi engine
16
+ # @see {Landline::Template#new}
17
+ # @param freeze [Boolean] whether to use frozen string literal
18
+ # @param capture [Boolean] whether to enable output capturing
19
+ def erubi(input, vars = {}, freeze: true, capture: false)
20
+ Landline::Templates::Erubi.new(input,
21
+ vars,
22
+ parent: @origin,
23
+ freeze: freeze,
24
+ capture: capture)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landline
4
+ module DSL
5
+ # Methods shared by probes, preprocessors and filters.
6
+ module CommonMethods
7
+ # Stop execution and generate a boilerplate response with the given code
8
+ # @param errorcode [Integer]
9
+ # @param backtrace [Array(String), nil]
10
+ # @raise [UncaughtThrowError] throws :finish to return back to Server
11
+ def die(errorcode, backtrace: nil)
12
+ throw :finish, [errorcode].append(
13
+ *(@origin.properties["handle.#{errorcode}"] or
14
+ @origin.properties["handle.default"]).call(
15
+ errorcode,
16
+ backtrace: backtrace
17
+ )
18
+ )
19
+ end
20
+
21
+ # Bounce request to the next handler
22
+ # @raise [UncaughtThrowError] throws :break to get out of the callback
23
+ def bounce
24
+ throw :break
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landline
4
+ # Shared DSL methods
5
+ module DSL
6
+ # Common path methods
7
+ module PathMethods
8
+ # Set path index
9
+ # @param index [Array,String]
10
+ def index(index)
11
+ case index
12
+ when Array
13
+ @origin.properties['index'] = index
14
+ when String
15
+ @origin.properties['index'] = [index]
16
+ else
17
+ raise ArgumentError, "index should be an Array or a String"
18
+ end
19
+ end
20
+
21
+ # Set root path (appends matched part of the path).
22
+ # @param path [String]
23
+ def root(path)
24
+ @origin.root = path
25
+ end
26
+
27
+ # Set root path (without appending matched part).
28
+ # @param path [String]
29
+ def remap(path)
30
+ @origin.remap = path
31
+ end
32
+
33
+ # Add a preprocessor to the path.
34
+ # Does not modify path execution.
35
+ # @param block [#call]
36
+ # @yieldparam request [Landline::Request]
37
+ def preprocess(&block)
38
+ @origin.preprocess(&block)
39
+ block
40
+ end
41
+
42
+ # Add a postprocessor to the path.
43
+ # @param block [#call]
44
+ # @yieldparam request [Landline::Request]
45
+ # @yieldparam response [Landline::Response]
46
+ def postprocess(&block)
47
+ @origin.postprocess(&block)
48
+ block
49
+ end
50
+
51
+ # Add a filter to the path.
52
+ # Blocks path access if a filter returns false.
53
+ # @param block [#call]
54
+ # @yieldparam request [Landline::Request]
55
+ def filter(&block)
56
+ @origin.filter(&block)
57
+ block
58
+ end
59
+
60
+ # Include an application as a child of path.
61
+ # @param filename [String]
62
+ def plugin(filename)
63
+ self.define_singleton_method(:run) do |object|
64
+ unless object.is_a? Landline::Node
65
+ raise ArgumentError, "not a node instance or subclass instance"
66
+ end
67
+
68
+ object
69
+ end
70
+ @origin.children.append(self.instance_eval(File.read(filename)))
71
+ self.singleton_class.undef_method :run
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../response'
4
+ require_relative '../util/multipart'
5
+ require_relative '../util/parseutils'
6
+ require_relative '../util/html'
7
+
8
+ module Landline
9
+ module DSL
10
+ # Common methods for Probe objects
11
+ module ProbeMethods
12
+ # Get the current request
13
+ # @return [Landline::Request]
14
+ def request
15
+ @origin.request
16
+ end
17
+
18
+ # Set response status (generate response if one doesn't exist yet)
19
+ # @param status [Integer] http status code
20
+ def status(status)
21
+ @origin.response = (@origin.response or Landline::Response.new)
22
+ @origin.response.status = status
23
+ end
24
+
25
+ alias code status
26
+
27
+ # Set response header (generate response if one doesn't exist yet)
28
+ # @param key [String] header name
29
+ # @param value [String] header value
30
+ def header(key, value)
31
+ return status(value) if key.downcase == "status"
32
+
33
+ unless key.match(Landline::Util::HeaderRegexp::TOKEN)
34
+ raise ArgumentError, "header key has invalid characters"
35
+ end
36
+
37
+ unless value&.match(Landline::Util::HeaderRegexp::PRINTABLE)
38
+ raise ArgumentError, "value key has invalid characters"
39
+ end
40
+
41
+ @origin.response = (@origin.response or Landline::Response.new)
42
+ key = key.downcase
43
+ @origin.response.add_header(key, value)
44
+ end
45
+
46
+ # Delete a header value from the headers hash
47
+ # If no value is provided, deletes all key entries
48
+ # @param key [String] header name
49
+ # @param value [String, nil] header value
50
+ def delete_header(key, value = nil)
51
+ return unless @origin.response
52
+
53
+ return if key.downcase == "status"
54
+
55
+ unless key.match(Landline::Util::HeaderRegexp::TOKEN)
56
+ raise ArgumentError, "header key has invalid characters"
57
+ end
58
+
59
+ unless value&.match(Landline::Util::HeaderRegexp::PRINTABLE)
60
+ raise ArgumentError, "value key has invalid characters"
61
+ end
62
+
63
+ @origin.response.delete_header(key, value)
64
+ end
65
+
66
+ # Set response cookie
67
+ # @see Landline::Cookie.new
68
+ def cookie(*params, **options)
69
+ @origin.response = (@origin.response or Landline::Response.new)
70
+ @origin.response.add_cookie(
71
+ Landline::Cookie.new(*params, **options)
72
+ )
73
+ end
74
+
75
+ # Delete a cookie
76
+ # If no value is provided, deletes all cookies with the same key
77
+ # @param key [String] cookie key
78
+ # @param value [String, nil] cookie.value
79
+ def delete_cookie(key, value = nil)
80
+ return unless @origin.response
81
+
82
+ @origin.response.delete_cookie(key, value)
83
+ end
84
+
85
+ # Checks if current request has multipart/form-data associated with it
86
+ # @return [Boolean]
87
+ def form?
88
+ value, opts = Landline::Util::ParserCommon.parse_value(
89
+ request.headers["content-type"]
90
+ )
91
+ if value == "multipart/form-data" and
92
+ opts["boundary"]
93
+ true
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ # Returns formdata
100
+ # @return [Hash{String=>(String,Landline::Util::FormPart)}]
101
+ def form
102
+ _, opts = Landline::Util::ParserCommon.parse_value(
103
+ request.headers["content-type"]
104
+ )
105
+ Landline::Util::MultipartParser.new(
106
+ request.input, opts["boundary"]
107
+ ).to_h
108
+ end
109
+
110
+ # Open a file relative to current filepath
111
+ # @see File.open
112
+ def file(path, mode = "r", *all, &block)
113
+ File.open("#{request.filepath}/#{path}", mode, *all, &block)
114
+ end
115
+
116
+ # Escape HTML entities
117
+ # @see Landline::Util.escape_html
118
+ def escape_html(text)
119
+ Landline::Util.escape_html(text)
120
+ end
121
+
122
+ # Unescape HTML entities
123
+ # @see Landline::Util.escape_html
124
+ def unescape_html(text)
125
+ Landline::Util.unescape_html(text)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'securerandom'
5
+
6
+ module Landline
7
+ module DSL
8
+ # Common methods for template contexts
9
+ module TemplateMethods
10
+ # Import a template part
11
+ def import(filepath)
12
+ @parent_template.import(file(filepath)).run
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pattern_matching/util'
4
+
5
+ module Landline
6
+ # Abstract class that reacts to request navigation.
7
+ # Does nothing by default, behaviour should be overriden through
8
+ # #reject and #process
9
+ # @abstract
10
+ class Node
11
+ # @param path [Object]
12
+ # @param filepath [Boolean] should Node modify request.filepath.
13
+ def initialize(path, parent:, filepath: true)
14
+ @pattern = Pattern.new(path).freeze
15
+ @properties = Landline::Util::Lookup.new(parent&.properties)
16
+ @root = nil
17
+ @remap = false
18
+ @modify_filepath = filepath
19
+ end
20
+
21
+ # Set Node file root (like root in Nginx)
22
+ # @param path [String]
23
+ def root=(path)
24
+ raise ArgumentError, "path should be a String" unless path.is_a? String
25
+
26
+ @properties["path"] = File.expand_path(path)
27
+ @root = File.expand_path(path)
28
+ end
29
+
30
+ # Set Node absolute file path (like alias in Nginx)
31
+ # @param path [String]
32
+ def remap=(path)
33
+ self.root = path
34
+ @remap = true
35
+ end
36
+
37
+ # Try to navigate the path. Run method callback in response.
38
+ # @param [Landline::Request]
39
+ # @return [Boolean]
40
+ def go(request)
41
+ # rejected at pattern
42
+ return reject(request) unless @pattern.match?(request.path)
43
+
44
+ request.push_state
45
+ path, splat, param = @pattern.match(request.path)
46
+ do_filepath(request, request.path.delete_suffix(path))
47
+ request.path = path
48
+ request.splat.append(*splat)
49
+ request.param.merge!(param)
50
+ value = process(request)
51
+ # rejected at callback - restore state
52
+ request.pop_state unless value
53
+ # finally, return process value
54
+ value
55
+ end
56
+
57
+ # Method callback on failed request navigation
58
+ # @param _request [Landline::Request]
59
+ # @return false
60
+ def reject(_request)
61
+ false
62
+ end
63
+
64
+ # Method callback on successful request navigation
65
+ # @param _request [Landline::Request]
66
+ # @return true
67
+ def process(_request)
68
+ true
69
+ end
70
+
71
+ attr_reader :remap, :root
72
+
73
+ private
74
+
75
+ # Process filepath for request
76
+ def do_filepath(request, path)
77
+ return unless @modify_filepath
78
+
79
+ if @root
80
+ request.filepath = "#{@root}/#{@remap ? '' : path}/"
81
+ else
82
+ request.filepath += "/#{path}/"
83
+ end
84
+ request.filepath.gsub!(/\/+/, "/")
85
+ end
86
+ end
87
+ end