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
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'uri'
4
+ require 'stringio'
5
+ require 'tempfile'
6
+ require_relative 'parseutils'
7
+ require_relative 'parsesorting'
8
+ require_relative 'html'
9
+
10
+ module Landline
11
+ module Util
12
+ # Valid element of form data with headers
13
+ # @!attribute headers [Hash] headers recevied from form data
14
+ # @!attribute name [String] name of the form part
15
+ # @!attribute data [String,nil] Data received in the field through form data
16
+ # @!attribute filename [String,nil] Original name of the sent file
17
+ # @!attribute filetype [String,nil] MIME-type of the file
18
+ # @!attribute tempfile [File,nil] Temporary file for storing sent file data.
19
+ FormPart = Struct.new(:data, :name, :filename,
20
+ :filetype, :tempfile, :headers) do
21
+ # Is this form part a file or plain data?
22
+ # @return [Boolean]
23
+ def file?
24
+ !tempfile.nil?
25
+ end
26
+
27
+ # Decode charset parameter
28
+ def decode(data)
29
+ data = Landline::Util.unescape_html(data)
30
+ return data unless self.headers['charset']
31
+
32
+ data.force_encoding(self.headers['charset']).encode("UTF-8")
33
+ end
34
+
35
+ # If FormPart is not a file, simplify to string.
36
+ # @return [FormPart, String]
37
+ def simplify
38
+ file? ? self : decode(self.data)
39
+ end
40
+ end
41
+
42
+ # A very naive implementation of a Multipart form parser.
43
+ class MultipartParser
44
+ include Landline::Util::ParserSorting
45
+ def initialize(io, boundary)
46
+ @input = io.is_a?(String) ? StringIO.new(io) : io
47
+ @boundary = boundary
48
+ @state = :idle
49
+ @data = []
50
+ end
51
+
52
+ # lord forgive me for what i'm about to do
53
+ # TODO: replace the god method with a state machine object
54
+ # rubocop:disable Metrics/*
55
+
56
+ # Parse multipart formdata
57
+ # @return [Array<FormPart, FormFile>]
58
+ def parse
59
+ return @data unless @data.empty?
60
+
61
+ while (line = @input.gets)
62
+ case @state
63
+ when :idle # waiting for valid boundary
64
+ if line == "--#{@boundary}\r\n"
65
+ # transition to :headers on valid boundary
66
+ @state = :headers
67
+ @data.append(FormPart.new(*([nil] * 5), {}))
68
+ end
69
+ when :headers # after valid boundary - checking for headers
70
+ if line == "\r\n"
71
+ # prepare form field and transition to :data or :file
72
+ @state = file?(@data[-1].headers) ? :file : :data
73
+ if @state == :data
74
+ setup_data_meta(@data[-1])
75
+ else
76
+ setup_file_meta(@data[-1])
77
+ end
78
+ next
79
+ end
80
+ push_header(line, @data[-1].headers)
81
+ when :data, :file # after headers - processing form data
82
+ if @data[-1].headers.empty?
83
+ # transition to :idle on empty headers
84
+ @state = :idle
85
+ next
86
+ end
87
+ if ["--#{@boundary}\r\n", "--#{@boundary}--\r\n"].include? line
88
+ # finalize and transition to either :headers or :idle
89
+ if @state == :file
90
+ @data[-1].tempfile.truncate(@data[-1].tempfile.size - 2)
91
+ @data[-1].tempfile.close
92
+ else
93
+ @data[-1].data.delete_suffix! "\r\n"
94
+ end
95
+ @state = line == "--#{@boundary}\r\n" ? :headers : :idle
96
+ @data.append(FormPart.new(*([nil] * 5), {}))
97
+ next
98
+ end
99
+ if @state == :data
100
+ @data[-1].data ||= ""
101
+ @data[-1].data << line
102
+ else
103
+ @data[-1].tempfile << line
104
+ end
105
+ end
106
+ end
107
+ @state = :idle
108
+ @data.pop
109
+ @data.freeze
110
+ end
111
+ # rubocop:enable Metrics/*
112
+
113
+ # Return a hash of current form.
114
+ # (equivalent to Query.parse but for multipart/form-data)
115
+ # @return [Hash]
116
+ def to_h
117
+ flatten(sort(gen_hash(parse)))
118
+ end
119
+
120
+ private
121
+
122
+ def gen_hash(array)
123
+ hash = {}
124
+ array.each do |formpart|
125
+ key = formpart.name.to_s
126
+ if key.match?(/.*\[\d*\]\Z/)
127
+ new_key, index = key.match(/(.*)\[(\d*)\]\Z/).to_a[1..]
128
+ hash[new_key] = [] unless hash[new_key]
129
+ hash[new_key].append([index, formpart.simplify])
130
+ else
131
+ hash[key] = formpart.simplify
132
+ end
133
+ end
134
+ hash
135
+ end
136
+
137
+ # Setup file metadata
138
+ # @part part [FormPart]
139
+ def setup_file_meta(part)
140
+ part.name = part.headers.dig("content-disposition", 1, "name")
141
+ part.filename = part.headers.dig("content-disposition", 1, "filename")
142
+ part.filetype = part.headers["content-type"]
143
+ part.tempfile = Tempfile.new
144
+ end
145
+
146
+ # Setup plain metadata
147
+ # @part part [FormPart]
148
+ def setup_data_meta(part)
149
+ part.name = part.headers.dig("content-disposition", 1, "name")
150
+ end
151
+
152
+ # Analyze headers to check if current data part is a file.
153
+ # @param headers_hash [Hash]
154
+ # @return [Boolean]
155
+ def file?(headers_hash)
156
+ if headers_hash.dig("content-disposition", 1, "filename") and
157
+ headers_hash['content-type']
158
+ return true
159
+ end
160
+
161
+ false
162
+ end
163
+
164
+ # Parse a header and append it to headers_hash
165
+ # @param line [String]
166
+ # @param headers_hash [Hash]
167
+ def push_header(line, headers_hash)
168
+ return unless line.match(/^[\w!#$%&'*+-.^_`|~]+:.*\r\n$/)
169
+
170
+ k, v = line.match(/^([\w!#$%&'*+-.^_`|~]+):(.*)\r\n$/).to_a[1..]
171
+ headers_hash[k.downcase] = Landline::Util::ParserCommon.parse_value(v)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Landline
4
+ module Util
5
+ # Internal library for generating form hashes
6
+ module ParserSorting
7
+ private
8
+
9
+ # Sort key-value pair arrays
10
+ def sort(hash)
11
+ hash.filter { |_, v| v.is_a? Array }.each do |_, value|
12
+ value.sort_by! { |array| array[0].to_i }
13
+ end
14
+ hash
15
+ end
16
+
17
+ # Flatten key-value pair arrays
18
+ def flatten(hash)
19
+ hash.transform_values do |value|
20
+ if value.is_a? Array
21
+ new_array = []
22
+ value.each do |k, v|
23
+ if k.match?(/\d+/) and k.to_i < new_array.size
24
+ new_array[k.to_i] = v
25
+ else
26
+ new_array.append(v)
27
+ end
28
+ end
29
+ new_array
30
+ else
31
+ value
32
+ end
33
+ end.to_h
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require_relative 'errors'
5
+
6
+ module Landline
7
+ module Util
8
+ # (not exactly precise) Regular expressions for some RFC definitions
9
+ module HeaderRegexp
10
+ # Matches the RFC2616 definiton of token
11
+ TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/.freeze
12
+ # Matches the RFC2616 definition of quoted-string
13
+ QUOTED = /"[\x0-\x7E&&[^\x1-\x8\xb-\x1f]]*(?<!\\)"/.freeze
14
+ # Matches any CHAR except CTLs
15
+ PRINTCHAR = /[\x2-\x7E]/.freeze
16
+ # Matches 1 or more CHARs excluding CTLs
17
+ PRINTABLE = /#{PRINTCHAR}+/o.freeze
18
+ # Matches the RFC6265 definition of a cookie-octet
19
+ COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/.freeze
20
+ COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o.freeze
21
+ COOKIE_NAME = TOKEN
22
+ # Matches the RFC6265 definition of cookie-pair.
23
+ # Captures name (1) and value (2).
24
+ COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\Z/o.freeze
25
+ # Matches a very abstract definition of a quoted header paramter.
26
+ # Captures name (1) and value (2).
27
+ PARAM_QUOTED = /\A(#{TOKEN})=?(#{QUOTED}|#{PRINTCHAR}*)\Z/o.freeze
28
+ # Matches a very abstract definition of a header parameter.
29
+ # Captures name (1) and value (2).
30
+ PARAM = /\A(#{TOKEN})=?(#{PRINTCHAR}*)\Z/o.freeze
31
+ # Specifically matches cookie parameters
32
+ COOKIE_PARAM = /\A(#{TOKEN})=?(#{QUOTED}|#{COOKIE_OCTET})\Z/o.freeze
33
+ end
34
+
35
+ # Module for all things related to parsing HTTP and related syntax.
36
+ module ParserCommon
37
+ # #strftime parameter to return a correct RFC 1123 date.
38
+ RFC1123_DATE = "%a, %d %b %Y %H:%M:%S GMT"
39
+
40
+ # rubocop:disable Metrics/MethodLength
41
+
42
+ # Parse parametrized header values.
43
+ # This method will try the best attempt at decoding parameters.
44
+ # However, it does no decoding on the first argument.
45
+ # @param input [String]
46
+ # @param sep [String, Regexp]
47
+ # @param unquote [Boolean] interpret params as possibly quoted
48
+ # @param regexp [Regexp,nil] override param matching regexp
49
+ # @return [Array(String, Hash)]
50
+ def self.parse_value(input, sep: ";", unquote: false, regexp: nil)
51
+ parts = input.split(sep).map { |x| URI.decode_uri_component(x).strip }
52
+ base = parts.shift
53
+ opts = parts.map do |raw|
54
+ key, value = raw.match(if regexp
55
+ regexp
56
+ elsif unquote
57
+ HeaderRegexp::PARAM_QUOTED
58
+ else
59
+ HeaderRegexp::PARAM
60
+ end).to_a[1..]
61
+ value = case value
62
+ when "" then true
63
+ when /\A".*"\z/ then value.undump
64
+ else value
65
+ end
66
+ [key, value]
67
+ end.to_h
68
+ [base, opts]
69
+ end
70
+
71
+ # rubocop:enable Metrics/MethodLength
72
+
73
+ # Construct a parametrized header value.
74
+ # Does some input sanitization during construction
75
+ # @param input [String]
76
+ # @param opts [Hash]
77
+ # @return [String]
78
+ # @raise [Landline::ParsingError]
79
+ def self.make_value(input, opts, sep = ";")
80
+ output = input
81
+ unless input.match? HeaderRegexp::PRINTABLE
82
+ raise Landline::ParsingError, "input is not ascii printable"
83
+ end
84
+
85
+ opts.each do |key, value|
86
+ check_param(key, value)
87
+ newparam = if [String, Integer].include? value.class
88
+ "#{sep} #{key}=#{value}"
89
+ elsif value
90
+ "#{sep} #{key}"
91
+ end
92
+ output += newparam if newparam
93
+ end
94
+ output
95
+ end
96
+
97
+ # Checks if key and value are valid for constructing a parameter.
98
+ # Raises an error if that is not possible.
99
+ def self.check_param(key, value)
100
+ unless key.match? HeaderRegexp::TOKEN
101
+ raise Landline::ParsingError, "key #{key} is not an RFC2616 token"
102
+ end
103
+
104
+ if value.is_a?(String) and value.match? HeaderRegexp::PARAM_QUOTED
105
+ raise Landline::ParsingError, "quoted param value #{value} is invalid"
106
+ end
107
+ end
108
+ private_class_method :check_param
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require_relative 'parsesorting'
5
+
6
+ module Landline
7
+ module Util
8
+ # Query string parser
9
+ class Query
10
+ include Landline::Util::ParserSorting
11
+ # @param query [String]
12
+ def initialize(query)
13
+ @query = query
14
+ end
15
+
16
+ # Shallow query parser (does not do PHP-like array keys)
17
+ # @return [Hash]
18
+ def parse_shallow
19
+ URI.decode_www_form(@query)
20
+ .sort_by { |array| array[0] }
21
+ .to_h
22
+ end
23
+
24
+ # Better(tm) query parser.
25
+ # Returns a hash with arrays.
26
+ # Key semantics:
27
+ #
28
+ # - `key=value` creates a key value pair
29
+ # - `key[]=value` appends `value` to an array named `key`
30
+ # - `key[index]=value` sets `value` at `index` of array named `key`
31
+ # @return [Hash]
32
+ def parse
33
+ construct_deep_hash(URI.decode_www_form(@query))
34
+ end
35
+
36
+ # Get key from query.
37
+ # @param key [String]
38
+ # @return [String,Array]
39
+ def [](key)
40
+ (@cache ||= parse)[key]
41
+ end
42
+
43
+ private
44
+
45
+ # Construct a hash with array support
46
+ def construct_deep_hash(array)
47
+ flatten(sort(group(array)))
48
+ end
49
+
50
+ # Assign values to keys in a new hash and group arrayable keys
51
+ def group(array)
52
+ hash = {}
53
+ array.each do |key, value|
54
+ if key.match?(/.*\[\d*\]\Z/)
55
+ new_key, index = key.match(/(.*)\[(\d*)\]\Z/).to_a[1..]
56
+ hash[new_key] = [] unless hash[new_key]
57
+ hash[new_key].append([index, value])
58
+ else
59
+ hash[key] = value
60
+ end
61
+ end
62
+ hash
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/landline.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'landline/server'
4
+ require_relative 'landline/path'
5
+ require_relative 'landline/probe'
6
+ require_relative 'landline/request'
7
+ require_relative 'landline/response'
8
+ require_relative 'landline/template'
9
+
10
+ # Landline is a hideously simple ruby web framework
11
+ module Landline
12
+ # Landline version
13
+ VERSION = '0.9 "Moonsong" (beta/rewrite)'
14
+
15
+ # Landline branding and version
16
+ VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n"
17
+
18
+ # Landline copyright
19
+ COPYRIGHT = "Copyright 2023 Yessiest"
20
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: landline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.2
5
+ platform: ruby
6
+ authors:
7
+ - Yessiest
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun.
15
+ It runs on any HTTP server that supports the Rack 3.0 protocol.
16
+ It is usable for many menial tasks, and as long as it continues to be fun, it will keep growing.
17
+ email: yessiest@text.512mb.org
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files:
21
+ - HACKING.md
22
+ - LAYOUT.md
23
+ - LICENSE.md
24
+ - README.md
25
+ files:
26
+ - HACKING.md
27
+ - LAYOUT.md
28
+ - LICENSE.md
29
+ - README.md
30
+ - lib/landline.rb
31
+ - lib/landline/dsl/constructors_path.rb
32
+ - lib/landline/dsl/constructors_probe.rb
33
+ - lib/landline/dsl/methods_common.rb
34
+ - lib/landline/dsl/methods_path.rb
35
+ - lib/landline/dsl/methods_probe.rb
36
+ - lib/landline/dsl/methods_template.rb
37
+ - lib/landline/node.rb
38
+ - lib/landline/path.rb
39
+ - lib/landline/pattern_matching.rb
40
+ - lib/landline/pattern_matching/glob.rb
41
+ - lib/landline/pattern_matching/rematch.rb
42
+ - lib/landline/pattern_matching/util.rb
43
+ - lib/landline/probe.rb
44
+ - lib/landline/probe/handler.rb
45
+ - lib/landline/probe/http_method.rb
46
+ - lib/landline/probe/serve_handler.rb
47
+ - lib/landline/request.rb
48
+ - lib/landline/response.rb
49
+ - lib/landline/server.rb
50
+ - lib/landline/template.rb
51
+ - lib/landline/template/erb.rb
52
+ - lib/landline/template/erubi.rb
53
+ - lib/landline/util/cookie.rb
54
+ - lib/landline/util/errors.rb
55
+ - lib/landline/util/html.rb
56
+ - lib/landline/util/lookup.rb
57
+ - lib/landline/util/mime.rb
58
+ - lib/landline/util/multipart.rb
59
+ - lib/landline/util/parsesorting.rb
60
+ - lib/landline/util/parseutils.rb
61
+ - lib/landline/util/query.rb
62
+ homepage: https://adastra7.net/git/Yessiest/landline
63
+ licenses:
64
+ - AGPL-3.0
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.3.25
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Elegant HTTP DSL
85
+ test_files: []