tynn 2.0.0.beta3 → 2.0.0.beta4
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/LICENSE +1 -1
- data/README.md +83 -660
- data/lib/tynn.rb +186 -50
- data/lib/tynn/environment.rb +1 -84
- data/lib/tynn/json.rb +24 -25
- data/lib/tynn/render.rb +72 -33
- data/lib/tynn/request.rb +2 -82
- data/lib/tynn/response.rb +4 -118
- data/lib/tynn/secure_headers.rb +2 -24
- data/lib/tynn/session.rb +4 -87
- data/lib/tynn/ssl.rb +12 -71
- data/lib/tynn/static.rb +1 -19
- data/lib/tynn/test.rb +2 -125
- data/lib/tynn/utils.rb +51 -8
- data/lib/tynn/version.rb +1 -1
- data/lib/tynn/x/versioning.rb +29 -0
- data/test/json_test.rb +197 -7
- data/test/render_test.rb +56 -8
- data/test/request_test.rb +2 -0
- data/test/response_test.rb +7 -3
- data/test/routing_test.rb +56 -4
- data/test/settings_test.rb +31 -0
- data/test/versioning_test.rb +201 -0
- metadata +10 -35
- data/lib/tynn/base.rb +0 -434
data/lib/tynn/request.rb
CHANGED
@@ -3,57 +3,15 @@
|
|
3
3
|
require "rack/request"
|
4
4
|
|
5
5
|
class Tynn
|
6
|
-
# It provides convenience methods for pulling out information from a request.
|
7
|
-
#
|
8
|
-
# env = {
|
9
|
-
# "REQUEST_METHOD" => "GET",
|
10
|
-
# "QUERY_STRING" => "q=great",
|
11
|
-
# # ...
|
12
|
-
# }
|
13
|
-
#
|
14
|
-
# req = Tynn::Request.new(env)
|
15
|
-
#
|
16
|
-
# req.get? # => true
|
17
|
-
# req.path # => "/search"
|
18
|
-
# req.params # => { "q" => "great" }
|
19
|
-
#
|
20
6
|
class Request < Rack::Request
|
21
|
-
# Returns the content length of the request as an integer.
|
22
|
-
#
|
23
|
-
# req.headers["content-length"]
|
24
|
-
# # => "20"
|
25
|
-
#
|
26
|
-
# req.content_length
|
27
|
-
# # => 20
|
28
|
-
#
|
29
7
|
def content_length
|
30
8
|
super.to_i
|
31
9
|
end
|
32
10
|
|
33
|
-
# Provides access to the request's HTTP headers.
|
34
|
-
#
|
35
|
-
# req.headers["content-type"] # => "application/json"
|
36
|
-
# req.headers.fetch("host") # => "example.org"
|
37
|
-
# req.headers.key?("https") # => true
|
38
|
-
#
|
39
11
|
def headers
|
40
12
|
@headers ||= Headers.new(env)
|
41
13
|
end
|
42
14
|
|
43
|
-
# Provides a uniform way to access HTTP headers from the request.
|
44
|
-
#
|
45
|
-
# headers = Tynn::Request::Headers.new(
|
46
|
-
# "CONTENT_TYPE" => "appplication/json"
|
47
|
-
# "HTTP_HOST" => "example.org"
|
48
|
-
# "HTTPS" => "on"
|
49
|
-
# )
|
50
|
-
#
|
51
|
-
# headers["content-type"] # => "application/json"
|
52
|
-
# headers.fetch("host") # => "example.org"
|
53
|
-
# headers.key?("https") # => true
|
54
|
-
#
|
55
|
-
# This helper class is used by Tynn::Request#headers.
|
56
|
-
#
|
57
15
|
class Headers
|
58
16
|
CGI_VARIABLES = %w(
|
59
17
|
AUTH_TYPE
|
@@ -74,58 +32,20 @@ class Tynn
|
|
74
32
|
SERVER_PORT
|
75
33
|
SERVER_PROTOCOL
|
76
34
|
SERVER_SOFTWARE
|
77
|
-
).freeze
|
35
|
+
).freeze
|
78
36
|
|
79
|
-
def initialize(env)
|
37
|
+
def initialize(env)
|
80
38
|
@env = env
|
81
39
|
end
|
82
40
|
|
83
|
-
# Returns the value for the given <tt>key</tt> mapped
|
84
|
-
# to the request environment (<tt>req.env</tt>).
|
85
|
-
#
|
86
|
-
# req.headers["content-type"]
|
87
|
-
# # => "application/json"
|
88
|
-
#
|
89
|
-
# req.headers["host"]
|
90
|
-
# # => "127.0.0.1"
|
91
|
-
#
|
92
41
|
def [](key)
|
93
42
|
@env[transform_key(key)]
|
94
43
|
end
|
95
44
|
|
96
|
-
# Returns the value for the given <tt>key</tt> mapped
|
97
|
-
# to the request environment. If the key is not found
|
98
|
-
# and a optional argument or code block is not provided,
|
99
|
-
# raises a <tt>KeyError</tt> exception. If an optional
|
100
|
-
# argument is provided, then it returns its value. If
|
101
|
-
# a code block is provided, then it will be run and its
|
102
|
-
# result returned.
|
103
|
-
#
|
104
|
-
# req.headers.fetch("content-type")
|
105
|
-
# # => "application/json"
|
106
|
-
#
|
107
|
-
# req.headers.fetch("https")
|
108
|
-
# # => KeyError: key not found: "HTTPS"
|
109
|
-
#
|
110
|
-
# req.headers.fetch("https", "")
|
111
|
-
# # => ""
|
112
|
-
#
|
113
|
-
# req.headers.fetch("https") { req.headers["x_forwarded_ssl"] }
|
114
|
-
# # => "on"
|
115
|
-
#
|
116
45
|
def fetch(key, *args, &block)
|
117
46
|
@env.fetch(transform_key(key), *args, &block)
|
118
47
|
end
|
119
48
|
|
120
|
-
# Returns <tt>true</tt> if the given <tt>key</tt> exists in the
|
121
|
-
# request environment. Otherwise, returns <tt>false</tt>.
|
122
|
-
#
|
123
|
-
# req.headers.key?("content-type")
|
124
|
-
# # => true
|
125
|
-
#
|
126
|
-
# req.headers.key?("https")
|
127
|
-
# # => false
|
128
|
-
#
|
129
49
|
def key?(key)
|
130
50
|
@env.key?(transform_key(key))
|
131
51
|
end
|
data/lib/tynn/response.rb
CHANGED
@@ -1,135 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Tynn
|
4
|
-
# It provides convenience methods to construct a Rack response.
|
5
|
-
#
|
6
|
-
# res = Tynn::Response.new
|
7
|
-
#
|
8
|
-
# res.status = 200
|
9
|
-
# res.headers["Content-Type"] = "text/plain"
|
10
|
-
# res.write("hei!")
|
11
|
-
#
|
12
|
-
# res.finish
|
13
|
-
# # => [200, { "Content-Type" => "text/plain", "Content-Length" => 4 }, ["hei!"]]
|
14
|
-
#
|
15
4
|
class Response
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
# res = Tynn::Response.new("Content-Type" => "text/plain")
|
20
|
-
#
|
21
|
-
# res.write("hei!")
|
22
|
-
#
|
23
|
-
# res.finish
|
24
|
-
# # => [200, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["hei!"]]
|
25
|
-
#
|
26
|
-
def initialize(headers = {})
|
27
|
-
@status = nil
|
5
|
+
def initialize(status = nil, headers = {}, body = [])
|
6
|
+
@status = status
|
28
7
|
@headers = headers
|
29
8
|
@body = []
|
30
9
|
@length = 0
|
10
|
+
|
11
|
+
body.each { |s| write(s) }
|
31
12
|
end
|
32
13
|
|
33
|
-
# Returns the status of the response.
|
34
|
-
#
|
35
|
-
# res.status # => 200
|
36
|
-
#
|
37
14
|
def status
|
38
15
|
@status
|
39
16
|
end
|
40
17
|
|
41
|
-
# Sets the status of the response.
|
42
|
-
#
|
43
|
-
# res.status = 200
|
44
|
-
#
|
45
18
|
def status=(status)
|
46
19
|
@status = status.to_i
|
47
20
|
end
|
48
21
|
|
49
|
-
# Returns a hash with the response headers.
|
50
|
-
#
|
51
|
-
# res.headers
|
52
|
-
# # => { "Content-Type" => "text/html", "Content-Length" => "42" }
|
53
|
-
#
|
54
22
|
def headers
|
55
23
|
@headers
|
56
24
|
end
|
57
25
|
|
58
|
-
# Returns the body of the response.
|
59
|
-
#
|
60
|
-
# res.body
|
61
|
-
# # => []
|
62
|
-
#
|
63
|
-
# res.write("there is")
|
64
|
-
# res.write("no try")
|
65
|
-
#
|
66
|
-
# res.body
|
67
|
-
# # => ["there is", "no try"]
|
68
|
-
#
|
69
26
|
def body
|
70
27
|
@body
|
71
28
|
end
|
72
29
|
|
73
|
-
# Returns response <tt>Content-Length</tt> header as an integer. If it
|
74
|
-
# is not present, it returns <tt>nil</tt>.
|
75
|
-
#
|
76
|
-
# res.headers["Content-Length"] # => "42"
|
77
|
-
# res.content_length # => 42
|
78
|
-
#
|
79
30
|
def content_length
|
80
31
|
(c = @headers["Content-Length"]) ? c.to_i : c
|
81
32
|
end
|
82
33
|
|
83
|
-
# Returns response <tt>Content-Type</tt> header.
|
84
|
-
#
|
85
|
-
# res.content_type = "text/html"
|
86
|
-
# res.content_type # => "text/html"
|
87
|
-
#
|
88
34
|
def content_type
|
89
35
|
@headers["Content-Type"]
|
90
36
|
end
|
91
37
|
|
92
|
-
# Sets response <tt>Content-Type</tt> header to <tt>type</tt>.
|
93
|
-
#
|
94
|
-
# res.content_type = "text/html"
|
95
|
-
# res.content_type # => "text/html"
|
96
|
-
#
|
97
38
|
def content_type=(type)
|
98
39
|
@headers["Content-Type"] = type
|
99
40
|
end
|
100
41
|
|
101
|
-
# Returns response <tt>Location</tt> header.
|
102
|
-
#
|
103
|
-
# res.location = "/users"
|
104
|
-
# res.location # => "/users"
|
105
|
-
#
|
106
42
|
def location
|
107
43
|
@headers["Location"]
|
108
44
|
end
|
109
45
|
|
110
|
-
# Sets response <tt>Location</tt> header to <tt>url</tt>.
|
111
|
-
#
|
112
|
-
# res.content_type = "text/html"
|
113
|
-
# res.content_type # => "text/html"
|
114
|
-
#
|
115
46
|
def location=(url)
|
116
47
|
@headers["Location"] = url
|
117
48
|
end
|
118
49
|
|
119
|
-
# Appends <tt>str</tt> to the response body and updates the
|
120
|
-
# <tt>Content-Length</tt> header.
|
121
|
-
#
|
122
|
-
# res.body # => []
|
123
|
-
#
|
124
|
-
# res.write("foo")
|
125
|
-
# res.write("bar")
|
126
|
-
#
|
127
|
-
# res.body
|
128
|
-
# # => ["foo", "bar"]
|
129
|
-
#
|
130
|
-
# res.headers["Content-Length"]
|
131
|
-
# # => 6
|
132
|
-
#
|
133
50
|
def write(str)
|
134
51
|
s = str.to_s
|
135
52
|
|
@@ -142,43 +59,12 @@ class Tynn
|
|
142
59
|
nil
|
143
60
|
end
|
144
61
|
|
145
|
-
# Returns an Array with three elements: the status, headers and body.
|
146
|
-
# If the status is not set, the status is set to <tt>404</tt> if empty body,
|
147
|
-
# otherwise the status is set to <tt>200</tt>.
|
148
|
-
#
|
149
|
-
# res.status = 200
|
150
|
-
# res.finish
|
151
|
-
# # => [200, {}, []]
|
152
|
-
#
|
153
|
-
# res.status = nil
|
154
|
-
# res.finish
|
155
|
-
# # => [404, {}, []]
|
156
|
-
#
|
157
|
-
# res.status = nil
|
158
|
-
# res.content_type = "text/html"
|
159
|
-
# res.write("hei!")
|
160
|
-
# res.finish
|
161
|
-
# # => [200, { "Content-Type" => "text/html", "Content-Length" => "4" }, ["hei!"]]
|
162
|
-
#
|
163
62
|
def finish
|
164
63
|
@status ||= ((@body.empty?) ? 404 : 200)
|
165
64
|
|
166
65
|
[@status, @headers, @body]
|
167
66
|
end
|
168
67
|
|
169
|
-
# Sets the <tt>Location</tt> header to <tt>url</tt> and updates the status
|
170
|
-
# to <tt>status</tt>. By default, <tt>status</tt> is <tt>302</tt>.
|
171
|
-
#
|
172
|
-
# res.redirect("/path")
|
173
|
-
#
|
174
|
-
# res.status # => 302
|
175
|
-
# res.location # => "/path"
|
176
|
-
#
|
177
|
-
# res.redirect("https://google.com", 303)
|
178
|
-
#
|
179
|
-
# res.status # => 303
|
180
|
-
# res.location # => "https://google.com"
|
181
|
-
#
|
182
68
|
def redirect(path, status = 302)
|
183
69
|
self.status = status
|
184
70
|
self.location = path
|
data/lib/tynn/secure_headers.rb
CHANGED
@@ -1,36 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Tynn
|
4
|
-
# Adds the following security related HTTP headers:
|
5
|
-
#
|
6
|
-
# [X-Content-Type-Options]
|
7
|
-
# Prevents IE and Chrome from {content type sniffing}[https://msdn.microsoft.com/library/gg622941(v=vs.85).aspx].
|
8
|
-
# Defaults to <tt>"nosniff"</tt>.
|
9
|
-
#
|
10
|
-
# [X-Frame-Options]
|
11
|
-
# Provides {Clickjacking}[https://www.owasp.org/index.php/Clickjacking]
|
12
|
-
# protection. Defaults to <tt>"deny"</tt>.
|
13
|
-
#
|
14
|
-
# [X-XSS-Protection]
|
15
|
-
# Enables the XSS protection filter built into IE, Chrome and Safari.
|
16
|
-
# This filter is usually enabled by default, the use of this header is to
|
17
|
-
# re-enable it if it was turned off by the user. Defaults to <tt>"1; mode=block"</tt>.
|
18
|
-
#
|
19
|
-
# <tt></tt>
|
20
|
-
#
|
21
|
-
# require "tynn"
|
22
|
-
# require "tynn/secure_headers"
|
23
|
-
#
|
24
|
-
# Tynn.plugin(Tynn::SecureHeaders)
|
25
|
-
#
|
26
4
|
module SecureHeaders
|
27
5
|
HEADERS = {
|
28
6
|
"X-Content-Type-Options" => "nosniff",
|
29
7
|
"X-Frame-Options" => "deny",
|
30
8
|
"X-XSS-Protection" => "1; mode=block"
|
31
|
-
}.freeze
|
9
|
+
}.freeze
|
32
10
|
|
33
|
-
def self.setup(app)
|
11
|
+
def self.setup(app)
|
34
12
|
app.set!(:default_headers, HEADERS.merge(app.default_headers))
|
35
13
|
end
|
36
14
|
end
|
data/lib/tynn/session.rb
CHANGED
@@ -4,94 +4,20 @@ require "rack/session/cookie"
|
|
4
4
|
require_relative "utils"
|
5
5
|
|
6
6
|
class Tynn
|
7
|
-
# Adds simple cookie based session management. You can pass a secret
|
8
|
-
# token to sign the cookie data, thus unauthorized means can't alter it.
|
9
|
-
#
|
10
|
-
# require "tynn"
|
11
|
-
# require "tynn/session"
|
12
|
-
#
|
13
|
-
# Tynn.plugin(Tynn::Session, secret: "__change_me_not_secure__")
|
14
|
-
#
|
15
|
-
# Tynn.define do
|
16
|
-
# on "login" do
|
17
|
-
# on post do
|
18
|
-
# # ...
|
19
|
-
#
|
20
|
-
# session[:user_id] = user.id
|
21
|
-
#
|
22
|
-
# res.redirect("/admin")
|
23
|
-
# end
|
24
|
-
# end
|
25
|
-
# end
|
26
|
-
#
|
27
|
-
# The following command generates a cryptographically secure secret ready
|
28
|
-
# to use:
|
29
|
-
#
|
30
|
-
# $ ruby -r securerandom -e "puts SecureRandom.hex(64)"
|
31
|
-
#
|
32
|
-
# It's important to keep the token secret. Knowing the token allows an
|
33
|
-
# attacker to tamper the data. So, it's recommended to load the token
|
34
|
-
# from the environment.
|
35
|
-
#
|
36
|
-
# Tynn.plugin(Tynn::Session, secret: ENV["SESSION_SECRET"])
|
37
|
-
#
|
38
|
-
# Under the hood, Tynn::Session uses the <tt>Rack::Session::Cookie</tt>
|
39
|
-
# middleware. Thus, supports all the options available for this middleware:
|
40
|
-
#
|
41
|
-
# [key]
|
42
|
-
# The name of the cookie. Defaults to <tt>"rack.session"</tt>.
|
43
|
-
#
|
44
|
-
# [httponly]
|
45
|
-
# If <tt>true</tt>, sets the <tt>HttpOnly</tt> flag. This mitigates the
|
46
|
-
# risk of client side scripting accessing the cookie. Defaults to <tt>true</tt>.
|
47
|
-
#
|
48
|
-
# [secure]
|
49
|
-
# If <tt>true</tt>, sets the <tt>Secure</tt> flag. This tells the browser
|
50
|
-
# to only transmit the cookie over HTTPS. Defaults to <tt>false</tt>.
|
51
|
-
#
|
52
|
-
# [same_site]
|
53
|
-
# Disables third-party usage for cookies. There are two possible values
|
54
|
-
# <tt>:Lax</tt> and <tt>:Strict</tt>. In <tt>Strict</tt> mode, the cookie
|
55
|
-
# is restrain to any cross-site usage; in <tt>Lax</tt> mode, some cross-site
|
56
|
-
# usage is allowed. Defaults to <tt>:Lax</tt>. If <tt>nil</tt> is passed,
|
57
|
-
# the flag is not included. Check this article[http://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/]
|
58
|
-
# for more information. Supported by Chrome 51+.
|
59
|
-
#
|
60
|
-
# [expire_after]
|
61
|
-
# The lifespan of the cookie. If <tt>nil</tt>, the session cookie is temporary
|
62
|
-
# and is no retained after the browser is closed. Defaults to <tt>nil</tt>.
|
63
|
-
#
|
64
|
-
# <tt></tt>
|
65
|
-
#
|
66
|
-
# Tynn.plugin(
|
67
|
-
# Tynn::Session,
|
68
|
-
# key: "app",
|
69
|
-
# secret: ENV["SESSION_SECRET"],
|
70
|
-
# expire_after: 36_000, # seconds
|
71
|
-
# httponly: true,
|
72
|
-
# secure: true,
|
73
|
-
# same_site: :Strict
|
74
|
-
# )
|
75
|
-
#
|
76
7
|
module Session
|
77
|
-
SECRET_MIN_LENGTH = 30
|
8
|
+
SECRET_MIN_LENGTH = 30
|
78
9
|
|
79
|
-
def self.setup(app, options = {})
|
10
|
+
def self.setup(app, options = {})
|
80
11
|
secret = options[:secret]
|
81
12
|
|
82
13
|
if secret.nil?
|
83
|
-
Tynn::Utils.raise_error(
|
84
|
-
"Secret key is required",
|
85
|
-
error: ArgumentError,
|
86
|
-
tag: :no_secret_key
|
87
|
-
)
|
14
|
+
Tynn::Utils.raise_error("Secret key is required", ArgumentError)
|
88
15
|
end
|
89
16
|
|
90
17
|
if secret.length < SECRET_MIN_LENGTH
|
91
18
|
Tynn::Utils.raise_error(
|
92
19
|
"Secret key is shorter than #{ SECRET_MIN_LENGTH } characters",
|
93
|
-
|
94
|
-
tag: :short_secret_key
|
20
|
+
ArgumentError
|
95
21
|
)
|
96
22
|
end
|
97
23
|
|
@@ -103,15 +29,6 @@ class Tynn
|
|
103
29
|
end
|
104
30
|
|
105
31
|
module InstanceMethods
|
106
|
-
# Returns the session hash.
|
107
|
-
#
|
108
|
-
# session
|
109
|
-
# # => {}
|
110
|
-
#
|
111
|
-
# session[:foo] = "foo"
|
112
|
-
# session[:foo]
|
113
|
-
# # => "foo"
|
114
|
-
#
|
115
32
|
def session
|
116
33
|
req.session
|
117
34
|
end
|