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.
- checksums.yaml +7 -0
- data/HACKING.md +30 -0
- data/LAYOUT.md +59 -0
- data/LICENSE.md +660 -0
- data/README.md +159 -0
- data/lib/landline/dsl/constructors_path.rb +107 -0
- data/lib/landline/dsl/constructors_probe.rb +28 -0
- data/lib/landline/dsl/methods_common.rb +28 -0
- data/lib/landline/dsl/methods_path.rb +75 -0
- data/lib/landline/dsl/methods_probe.rb +129 -0
- data/lib/landline/dsl/methods_template.rb +16 -0
- data/lib/landline/node.rb +87 -0
- data/lib/landline/path.rb +157 -0
- data/lib/landline/pattern_matching/glob.rb +168 -0
- data/lib/landline/pattern_matching/rematch.rb +49 -0
- data/lib/landline/pattern_matching/util.rb +15 -0
- data/lib/landline/pattern_matching.rb +75 -0
- data/lib/landline/probe/handler.rb +56 -0
- data/lib/landline/probe/http_method.rb +74 -0
- data/lib/landline/probe/serve_handler.rb +39 -0
- data/lib/landline/probe.rb +62 -0
- data/lib/landline/request.rb +135 -0
- data/lib/landline/response.rb +140 -0
- data/lib/landline/server.rb +49 -0
- data/lib/landline/template/erb.rb +27 -0
- data/lib/landline/template/erubi.rb +36 -0
- data/lib/landline/template.rb +95 -0
- data/lib/landline/util/cookie.rb +150 -0
- data/lib/landline/util/errors.rb +11 -0
- data/lib/landline/util/html.rb +119 -0
- data/lib/landline/util/lookup.rb +37 -0
- data/lib/landline/util/mime.rb +1276 -0
- data/lib/landline/util/multipart.rb +175 -0
- data/lib/landline/util/parsesorting.rb +37 -0
- data/lib/landline/util/parseutils.rb +111 -0
- data/lib/landline/util/query.rb +66 -0
- data/lib/landline.rb +20 -0
- metadata +85 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'util/query'
|
5
|
+
require_relative 'util/cookie'
|
6
|
+
|
7
|
+
module Landline
|
8
|
+
# Request wrapper for Rack protocol
|
9
|
+
class Request
|
10
|
+
# @param env [Array]
|
11
|
+
def initialize(env)
|
12
|
+
# Should not be used under regular circumstances or depended upon.
|
13
|
+
@_original_env = env
|
14
|
+
# Rack environment variable bindings. Should be public and frozen.
|
15
|
+
init_request_params(env)
|
16
|
+
# Cookie hash
|
17
|
+
@cookies = Landline::Cookie.from_cookie_string(@headers['cookie'])
|
18
|
+
# Query parsing
|
19
|
+
@query = Landline::Util::Query.new(@query_string)
|
20
|
+
# Pattern matching parameters. Public, readable, unfrozen.
|
21
|
+
@param = {}
|
22
|
+
@splat = []
|
23
|
+
# Traversal route. Public and writable.
|
24
|
+
@path = URI.decode_www_form_component(env["PATH_INFO"].dup)
|
25
|
+
# File serving path. Public and writable.
|
26
|
+
@filepath = "/"
|
27
|
+
# Encapsulates all rack variables. Should not be public.
|
28
|
+
@rack = init_rack_vars(env)
|
29
|
+
# Internal navigation states. Private.
|
30
|
+
@states = []
|
31
|
+
# Postprocessors for current request
|
32
|
+
@postprocessors = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Run postprocessors
|
36
|
+
# @param response [Landline::Response]
|
37
|
+
def run_postprocessors(response)
|
38
|
+
@postprocessors.each do |postproc|
|
39
|
+
postproc.call(self, response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns request body (if POST data exists)
|
44
|
+
# @return [nil, String]
|
45
|
+
def body
|
46
|
+
@body ||= @rack.input&.read
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns raw Rack input object
|
50
|
+
# @return [IO] (May not entirely be compatible with IO, see Rack/SPEC.rdoc)
|
51
|
+
def input
|
52
|
+
@rack.input
|
53
|
+
end
|
54
|
+
|
55
|
+
# Push current navigation state (path, splat, param) onto state stack
|
56
|
+
def push_state
|
57
|
+
@states.push([@path, @param.dup, @splat.dup, @filepath.dup])
|
58
|
+
end
|
59
|
+
|
60
|
+
# Load last navigation state (path, splat, param) from state stack
|
61
|
+
def pop_state
|
62
|
+
@path, @param, @splat, @filepath = @states.pop
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :request_method, :script_name, :path_info, :server_name,
|
66
|
+
:server_port, :server_protocol, :headers, :param, :splat,
|
67
|
+
:postprocessors, :query, :cookies
|
68
|
+
attr_accessor :path, :filepath
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Initialize basic rack request parameters
|
73
|
+
# @param env [Hash]
|
74
|
+
def init_request_params(env)
|
75
|
+
@request_method = env["REQUEST_METHOD"]
|
76
|
+
@script_name = env["SCRIPT_NAME"]
|
77
|
+
@path_info = env["PATH_INFO"]
|
78
|
+
@query_string = env["QUERY_STRING"]
|
79
|
+
@server_name = env["SERVER_NAME"]
|
80
|
+
@server_port = env["SERVER_PORT"]
|
81
|
+
@server_protocol = env["SERVER_PROTOCOL"]
|
82
|
+
@headers = init_headers(env)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Initialize rack parameters struct
|
86
|
+
# @param env [Hash]
|
87
|
+
# @return Object
|
88
|
+
def init_rack_vars(env)
|
89
|
+
rack_vars = env.filter_map do |k, v|
|
90
|
+
[k.delete_prefix("rack."), v] if k.start_with? "rack."
|
91
|
+
end.to_h
|
92
|
+
return if rack_vars.empty?
|
93
|
+
|
94
|
+
rack_vars["multipart"] = init_multipart_vars(env)
|
95
|
+
rack_keys = rack_vars.keys
|
96
|
+
rack_keys_sym = rack_keys.map(&:to_sym)
|
97
|
+
Struct.new(*rack_keys_sym)
|
98
|
+
.new(*rack_vars.values_at(*rack_keys))
|
99
|
+
.freeze
|
100
|
+
end
|
101
|
+
|
102
|
+
# Initialize multipart parameters struct
|
103
|
+
# @param env [Hash]
|
104
|
+
# @return Object
|
105
|
+
def init_multipart_vars(env)
|
106
|
+
multipart_vars = env.filter_map do |k, v|
|
107
|
+
if k.start_with? "rack.multipart"
|
108
|
+
[k.delete_prefix("rack.multipart."), v]
|
109
|
+
end
|
110
|
+
end.to_h
|
111
|
+
return if multipart_vars.empty?
|
112
|
+
|
113
|
+
multipart_keys = multipart_vars.keys
|
114
|
+
multipart_keys_sym = multipart_keys.map(&:to_sym)
|
115
|
+
Struct.new(*multipart_keys_sym)
|
116
|
+
.new(*multipart_vars.values_at(*multipart_keys))
|
117
|
+
.freeze
|
118
|
+
end
|
119
|
+
|
120
|
+
# Iniitalize headers hash
|
121
|
+
# @param env [Hash]
|
122
|
+
# @return Hash
|
123
|
+
def init_headers(env)
|
124
|
+
headers = env.filter_map do |name, value|
|
125
|
+
[name.delete_prefix("HTTP_"), value] if name.start_with?("HTTP_")
|
126
|
+
end.to_h
|
127
|
+
headers.merge!({ "CONTENT-TYPE" => env["CONTENT_TYPE"],
|
128
|
+
"CONTENT-LENGTH" => env["CONTENT_LENGTH"],
|
129
|
+
"REMOTE_ADDR" => env["REMOTE_ADDR"] })
|
130
|
+
headers.transform_keys do |x|
|
131
|
+
x.downcase.gsub("_", "-") if x.is_a? String
|
132
|
+
end.freeze
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
# Rack protocol response wrapper.
|
5
|
+
class Response
|
6
|
+
@chunk_size = 1024
|
7
|
+
|
8
|
+
self.class.attr_accessor :chunk_size
|
9
|
+
|
10
|
+
# @param response [Array(Integer, Hash, Array), nil]
|
11
|
+
def initialize(response = nil)
|
12
|
+
@cookies = {}
|
13
|
+
if response
|
14
|
+
@status = response[0]
|
15
|
+
@headers = response[1]
|
16
|
+
@body = response[2]
|
17
|
+
else
|
18
|
+
@status = 200
|
19
|
+
@headers = {}
|
20
|
+
@body = []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return internal representation of Rack response
|
25
|
+
# @return [Array(Integer,Hash,Array)]
|
26
|
+
def finalize
|
27
|
+
@cookies.each do |_, cookie_array|
|
28
|
+
cookie_array.each do |cookie|
|
29
|
+
add_header("set-cookie", cookie.finalize)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
[@status, @headers, @body]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Make internal representation conformant
|
36
|
+
# @return [Landline::Response]
|
37
|
+
def validate
|
38
|
+
if [204, 304].include?(@status) or (100..199).include?(@status)
|
39
|
+
@headers.delete "content-length"
|
40
|
+
@headers.delete "content-type"
|
41
|
+
@body = []
|
42
|
+
elsif @headers.empty?
|
43
|
+
@headers = {
|
44
|
+
"content-length" => content_size,
|
45
|
+
"content-type" => "text/html"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
@body = self.class.chunk_body(@body) if @body.is_a? String
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add a cookie to the response
|
53
|
+
# @param cookie [Landline::Cookie]
|
54
|
+
def add_cookie(cookie)
|
55
|
+
if @cookies[cookie.key]
|
56
|
+
@cookies[cookie.key].append(cookie)
|
57
|
+
else
|
58
|
+
@cookies[cookie.key] = [cookie]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Delete a cookie
|
63
|
+
# If no value is provided, deletes all cookies with the same key
|
64
|
+
# @param key [String] cookie key
|
65
|
+
# @param value [String, nil] cookie value
|
66
|
+
def delete_cookie(key, value)
|
67
|
+
if value
|
68
|
+
@cookies[key].delete(value)
|
69
|
+
else
|
70
|
+
@cookies.delete(key)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add a header to the headers hash
|
75
|
+
# @param key [String] header name
|
76
|
+
# @param value [String] header value
|
77
|
+
def add_header(key, value)
|
78
|
+
if @headers[key].is_a? String
|
79
|
+
@headers[key] = [@headers[key], value]
|
80
|
+
elsif @headers[key].is_a? Array
|
81
|
+
@headers[key].append(value)
|
82
|
+
else
|
83
|
+
@headers[key] = value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Delete a header value from the headers hash
|
88
|
+
# If no value is provided, deletes all key entries
|
89
|
+
# @param key [String] header name
|
90
|
+
# @param value [String, nil] header value
|
91
|
+
def delete_header(key, value = nil)
|
92
|
+
if value and @headers[key].is_a? Array
|
93
|
+
@headers[key].delete(value)
|
94
|
+
else
|
95
|
+
@headers.delete(key)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_accessor :status, :headers, :body
|
100
|
+
|
101
|
+
# Ensure response correctness
|
102
|
+
# @param obj [String, Array, Landline::Response]
|
103
|
+
# @return Response
|
104
|
+
def self.convert(obj)
|
105
|
+
case obj
|
106
|
+
when Response
|
107
|
+
obj.validate
|
108
|
+
when Array
|
109
|
+
Response.new(obj).validate
|
110
|
+
when String, File, IO
|
111
|
+
Response.new([200,
|
112
|
+
{
|
113
|
+
"content-type" => "text/html"
|
114
|
+
},
|
115
|
+
obj]).validate
|
116
|
+
else
|
117
|
+
Response.new([404, {}, []])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Turn body into array of chunks
|
122
|
+
# @param text [String]
|
123
|
+
# @return [Array(String)]
|
124
|
+
def self.chunk_body(text)
|
125
|
+
text.chars.each_slice(@chunk_size).map(&:join)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# Try to figure out content length
|
131
|
+
# @return [Integer, nil]
|
132
|
+
def content_size
|
133
|
+
case @body
|
134
|
+
when String then @body.bytesize
|
135
|
+
when Array then @body.join.bytesize
|
136
|
+
when File then @body.size
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'path'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'util/html'
|
6
|
+
|
7
|
+
module Landline
|
8
|
+
class ServerContext < Landline::PathContext
|
9
|
+
end
|
10
|
+
|
11
|
+
# A specialized path that can be used directly as a Rack application.
|
12
|
+
class Server < Landline::Path
|
13
|
+
Context = ServerContext
|
14
|
+
|
15
|
+
# @param parent [Landline::Node, nil] Parent object to inherit properties to
|
16
|
+
# @param setup [#call] Setup block
|
17
|
+
def initialize(parent: nil, **args, &setup)
|
18
|
+
super("", parent: nil, **args, &setup)
|
19
|
+
return if parent
|
20
|
+
|
21
|
+
{
|
22
|
+
"index" => [],
|
23
|
+
"handle.default" => proc do |code, backtrace: nil|
|
24
|
+
page = Landline::Util.default_error_page(code, backtrace)
|
25
|
+
headers = {
|
26
|
+
"content-length": page.bytesize,
|
27
|
+
"content-type": "text/html"
|
28
|
+
}
|
29
|
+
[headers, page]
|
30
|
+
end,
|
31
|
+
"path" => "/"
|
32
|
+
}.each { |k, v| @properties[k] = v unless @properties[k] }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Rack ingress point.
|
36
|
+
# This should not be called under any circumstances twice in the same application,
|
37
|
+
# although server nesting for the purpose of creating virtual hosts is allowed.
|
38
|
+
# @param env [Hash]
|
39
|
+
# @return [Array(Integer,Hash,Array)]
|
40
|
+
def call(env)
|
41
|
+
request = Landline::Request.new(env)
|
42
|
+
response = catch(:finish) do
|
43
|
+
go(request)
|
44
|
+
end
|
45
|
+
request.run_postprocessors(response)
|
46
|
+
Response.convert(response).finalize
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require_relative '../template'
|
5
|
+
|
6
|
+
module Landline
|
7
|
+
module Templates
|
8
|
+
# ERB Template language adapter
|
9
|
+
class ERB < Landline::Template
|
10
|
+
# @see {Landline::Template#new}
|
11
|
+
def initialize(input, vars = nil, parent:)
|
12
|
+
super
|
13
|
+
varname = "_part_#{SecureRandom.hex(10)}".to_sym
|
14
|
+
while @binding.local_variable_defined? varname
|
15
|
+
varname = "_part_#{SecureRandom.hex(10)}".to_sym
|
16
|
+
end
|
17
|
+
@template = ::ERB.new(@template, eoutvar: varname)
|
18
|
+
@template.filename = input.is_a?(File) ? input.path : "(Inline)"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Run the template.
|
22
|
+
def run
|
23
|
+
@template.result @binding
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erubi'
|
4
|
+
require_relative '../template'
|
5
|
+
|
6
|
+
module Landline
|
7
|
+
module Templates
|
8
|
+
# Erubi (ERB) template language adapter
|
9
|
+
class Erubi < Landline::Template
|
10
|
+
# @see {Landline::Template#new}
|
11
|
+
def initialize(input,
|
12
|
+
vars = nil,
|
13
|
+
parent:,
|
14
|
+
freeze: true,
|
15
|
+
capture: false)
|
16
|
+
super(input, vars, parent: parent)
|
17
|
+
varname = "_part_#{SecureRandom.hex(10)}"
|
18
|
+
while @binding.local_variable_defined? varname.to_sym
|
19
|
+
varname = "_part_#{SecureRandom.hex(10)}"
|
20
|
+
end
|
21
|
+
properties = {
|
22
|
+
filename: input.is_a?(File) ? input.path : "(Inline)",
|
23
|
+
bufvar: varname,
|
24
|
+
freeze: freeze
|
25
|
+
}
|
26
|
+
engine = capture ? ::Erubi::CaptureEndEngine : ::Erubi::Engine
|
27
|
+
@template = engine.new(@template, properties)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Run the template.
|
31
|
+
def run
|
32
|
+
@binding.eval(@template.src)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'dsl/constructors_probe'
|
4
|
+
require_relative 'dsl/methods_common'
|
5
|
+
require_relative 'dsl/methods_probe'
|
6
|
+
require_relative 'dsl/methods_template'
|
7
|
+
|
8
|
+
module Landline
|
9
|
+
# All template engine adapters subclassed from Template
|
10
|
+
module Templates
|
11
|
+
autoload :ERB, "landline/template/erb"
|
12
|
+
autoload :Erubi, "landline/template/erubi"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Context for template engines
|
16
|
+
class TemplateContext
|
17
|
+
include Landline::DSL::ProbeConstructors
|
18
|
+
include Landline::DSL::ProbeMethods
|
19
|
+
include Landline::DSL::CommonMethods
|
20
|
+
include Landline::DSL::TemplateMethods
|
21
|
+
|
22
|
+
# @return [Binding]
|
23
|
+
def binding
|
24
|
+
Kernel.binding
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(parent, parent_template)
|
28
|
+
@origin = parent
|
29
|
+
@parent_template = parent_template
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Interface for Template engines
|
34
|
+
# @abstract does not represent any actual template engine.
|
35
|
+
class Template
|
36
|
+
# @param input [String, File] template text
|
37
|
+
# @param vars [Hash] local variables for tempalte
|
38
|
+
# @param parent [Landline::Node] parent node
|
39
|
+
def initialize(input, vars = {}, parent:)
|
40
|
+
@template = input.is_a?(File) ? input.read : input
|
41
|
+
@context = TemplateContext.new(parent, self)
|
42
|
+
@parent = parent
|
43
|
+
input.close if input.is_a? File
|
44
|
+
@binding = @context.binding
|
45
|
+
vars.each do |k, v|
|
46
|
+
@binding.local_variable_set(k, v)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set local variable
|
51
|
+
# @param key [Symbol]
|
52
|
+
# @param value [Object]
|
53
|
+
def local_variable_set(key, value)
|
54
|
+
@binding.local_variable_set(key, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get local variable
|
58
|
+
# @param key [Symbol]
|
59
|
+
# @return [Object]
|
60
|
+
def local_variable_get(key)
|
61
|
+
@binding.local_variable_get(key)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get an array of defined local variables
|
65
|
+
# @return [Array(Symbol)]
|
66
|
+
def local_variables
|
67
|
+
@binding.local_variables
|
68
|
+
end
|
69
|
+
|
70
|
+
# Override binding variables.
|
71
|
+
# @param vars [Hash{Symbol => Object}]
|
72
|
+
def override_locals(vars)
|
73
|
+
vars.each do |k, v|
|
74
|
+
@binding.local_variable_set(k, v)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Run the template
|
79
|
+
# @note This method is a stub.
|
80
|
+
def run
|
81
|
+
# ... (stub)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Import a template from within current template
|
85
|
+
def import(filepath)
|
86
|
+
newtemp = self.class.new(filepath, {}, parent: @parent)
|
87
|
+
newtemp.binding = @binding
|
88
|
+
newtemp
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
attr_accessor :binding
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'parseutils'
|
4
|
+
require_relative 'errors'
|
5
|
+
require 'date'
|
6
|
+
require 'openssl'
|
7
|
+
HeaderRegexp = Landline::Util::HeaderRegexp
|
8
|
+
ParserCommon = Landline::Util::ParserCommon
|
9
|
+
|
10
|
+
module Landline
|
11
|
+
# Utility class for handling cookies
|
12
|
+
class Cookie
|
13
|
+
# @param key [String] cookie name
|
14
|
+
# @param value [String] cookie value
|
15
|
+
# @param params [Hash] cookie parameters
|
16
|
+
# @option params [String] "domain"
|
17
|
+
# @option params [String] "path"
|
18
|
+
# @option params [boolean, nil] "secure" (false)
|
19
|
+
# @option params [boolean, nil] "httponly" (false)
|
20
|
+
# @option params [String] "samesite"
|
21
|
+
# @option params [String, Integer] "max-age"
|
22
|
+
# @option params [String, Date] "expires"
|
23
|
+
# @raise Landline::ParsingError invalid cookie parameters
|
24
|
+
def initialize(key, value, params = {})
|
25
|
+
unless key.match? HeaderRegexp::COOKIE_NAME
|
26
|
+
raise Landline::ParsingError, "invalid cookie key: #{key}"
|
27
|
+
end
|
28
|
+
|
29
|
+
unless value.match? HeaderRegexp::COOKIE_VALUE
|
30
|
+
raise Landline::ParsingError, "invalid cookie value: #{value}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Make param keys strings
|
34
|
+
params.transform_keys!(&:to_s)
|
35
|
+
|
36
|
+
# Primary cookie parameters
|
37
|
+
@key = key
|
38
|
+
@value = value
|
39
|
+
setup_params(params)
|
40
|
+
|
41
|
+
# Cookie signing parameters
|
42
|
+
setup_hmac(params)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert cookie to "Set-Cookie: " string representation.
|
46
|
+
# @return [String]
|
47
|
+
def finalize
|
48
|
+
sign(@hmac, algorithm: @algorithm, sep: @sep) if @hmac
|
49
|
+
ParserCommon.make_value(
|
50
|
+
"#{key.to_s.strip}=#{value.to_s.strip}",
|
51
|
+
{
|
52
|
+
"Domain" => @domain,
|
53
|
+
"Path" => @path,
|
54
|
+
"Expires" => @expires,
|
55
|
+
"Max-Age" => @maxage,
|
56
|
+
"SameSite" => @samesite,
|
57
|
+
"Secure" => @secure,
|
58
|
+
"HttpOnly" => @httponly
|
59
|
+
}
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Convert cookie to "Cookie: " string representation (no params)
|
64
|
+
# @return [String]
|
65
|
+
def finalize_short
|
66
|
+
sign(@hmac, algorithm: @algorithm, sep: @sep) if @hmac
|
67
|
+
"#{key.to_s.strip}=#{value.to_s.strip}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sign the cookie value with HMAC
|
71
|
+
# @param key [String] HMAC signing key
|
72
|
+
# @param algorithm [String] Hash algorithm to use
|
73
|
+
# @param sep [String] Hash separator
|
74
|
+
def sign(key, algorithm: "sha256", sep: "&")
|
75
|
+
@value += sep + ::OpenSSL::HMAC.base64digest(algorithm, key, @value)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Verify HMAC signature
|
79
|
+
# @param key [String] HMAC signing key
|
80
|
+
# @param algorithm [String] Hash algorithm
|
81
|
+
# @param sep [String] Hash separator
|
82
|
+
# @return [Boolean] whether value is signed and valid
|
83
|
+
def verify(key, algorithm: "sha256", sep: "&")
|
84
|
+
val, sig = @value.match(/\A(.*)#{sep}([A-Za-z0-9+\/=]+)\Z/).to_a[1..]
|
85
|
+
return false unless val and sig
|
86
|
+
|
87
|
+
sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val)
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_accessor :key, :value
|
91
|
+
attr_reader :domain, :path, :expires, :maxage, :samesite, :secure, :httponly
|
92
|
+
|
93
|
+
# Create cookie from a "Set-Cookie: " format
|
94
|
+
# @param data [String] value part of "Set-Cookie: " header
|
95
|
+
# @return [Cookie]
|
96
|
+
def self.from_setcookie_string(data)
|
97
|
+
kvpair, params = parse_value(data, regexp: HeaderRegexp::COOKIE_PARAM)
|
98
|
+
key, value = kvpair.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
|
99
|
+
Cookie.new(key, value, params)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create cookie(s) from a "Cookie: " format
|
103
|
+
# @param data [String] value part of "Cookie: " header
|
104
|
+
# @return [Hash{String => Cookie}]
|
105
|
+
def self.from_cookie_string(data)
|
106
|
+
hash = {}
|
107
|
+
return hash if data.nil?
|
108
|
+
|
109
|
+
data.split(";").map do |cookiestr|
|
110
|
+
key, value = cookiestr.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
|
111
|
+
cookie = Cookie.new(key, value)
|
112
|
+
if hash[cookie.key]
|
113
|
+
hash[cookie.key].append(cookie)
|
114
|
+
else
|
115
|
+
hash[cookie.key] = [cookie]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
hash
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def setup_hmac(params)
|
124
|
+
@hmac = params['hmac']
|
125
|
+
@algorithm = (params['algorithm'] or "sha256")
|
126
|
+
@sep = (params['sep'] or "&")
|
127
|
+
end
|
128
|
+
|
129
|
+
def setup_params(params)
|
130
|
+
# Extended cookie params
|
131
|
+
params.transform_keys!(&:downcase)
|
132
|
+
convert_date(params)
|
133
|
+
@domain = params['domain']&.remove_prefix(".")
|
134
|
+
@path = params['path']
|
135
|
+
@secure = !params['secure'].nil?
|
136
|
+
@httponly = !params['httponly'].nil?
|
137
|
+
@samesite = params['samesite'] or "None"
|
138
|
+
end
|
139
|
+
|
140
|
+
def convert_date(params)
|
141
|
+
maxage = params['max-age']
|
142
|
+
expires = params['expires']
|
143
|
+
@maxage = maxage.to_i if maxage&.match?(/\A\d+\z/)
|
144
|
+
@expires = case expires
|
145
|
+
when Date then date.ctime(HeaderRegexp::RFC1123_DATE)
|
146
|
+
when String then expires if Date.httpdate(expires)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|