tynn 2.0.0.alpha → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +168 -29
- data/lib/tynn.rb +0 -4
- data/lib/tynn/base.rb +368 -24
- data/lib/tynn/errors.rb +7 -0
- data/lib/tynn/json.rb +18 -5
- data/lib/tynn/render.rb +36 -25
- data/lib/tynn/request.rb +86 -15
- data/lib/tynn/response.rb +214 -12
- data/lib/tynn/secure_headers.rb +2 -6
- data/lib/tynn/session.rb +14 -5
- data/lib/tynn/ssl.rb +19 -16
- data/lib/tynn/test.rb +45 -38
- data/lib/tynn/utils.rb +15 -0
- data/lib/tynn/version.rb +1 -1
- data/test/default_headers_test.rb +1 -1
- data/test/environment_test.rb +1 -1
- data/test/helper.rb +9 -0
- data/test/json_test.rb +21 -6
- data/test/middleware_test.rb +23 -13
- data/test/plugin_test.rb +1 -1
- data/test/render_test.rb +24 -15
- data/test/request_headers_test.rb +8 -4
- data/test/request_test.rb +9 -0
- data/test/response_test.rb +217 -0
- data/test/routing_test.rb +128 -38
- data/test/secure_headers_test.rb +1 -1
- data/test/session_test.rb +6 -6
- data/test/settings_test.rb +3 -3
- data/test/ssl_test.rb +3 -3
- data/test/static_test.rb +1 -1
- metadata +14 -24
- data/lib/tynn/default_headers.rb +0 -50
- data/lib/tynn/settings.rb +0 -107
data/lib/tynn/secure_headers.rb
CHANGED
@@ -9,10 +9,7 @@ class Tynn
|
|
9
9
|
#
|
10
10
|
# [X-Frame-Options]
|
11
11
|
# Provides {Clickjacking}[https://www.owasp.org/index.php/Clickjacking]
|
12
|
-
# protection. Defaults to <tt>"
|
13
|
-
#
|
14
|
-
# [X-Permitted-Cross-Domain-Policies]
|
15
|
-
# Restricts Adobe Flash Player's access to data. Defaults to <tt>"none"</tt>.
|
12
|
+
# protection. Defaults to <tt>"deny"</tt>.
|
16
13
|
#
|
17
14
|
# [X-XSS-Protection]
|
18
15
|
# Enables the XSS protection filter built into IE, Chrome and Safari.
|
@@ -29,8 +26,7 @@ class Tynn
|
|
29
26
|
module SecureHeaders
|
30
27
|
HEADERS = {
|
31
28
|
"X-Content-Type-Options" => "nosniff",
|
32
|
-
"X-Frame-Options" => "
|
33
|
-
"X-Permitted-Cross-Domain-Policies" => "none",
|
29
|
+
"X-Frame-Options" => "deny",
|
34
30
|
"X-XSS-Protection" => "1; mode=block"
|
35
31
|
}.freeze # :nodoc:
|
36
32
|
|
data/lib/tynn/session.rb
CHANGED
@@ -10,8 +10,8 @@ class Tynn
|
|
10
10
|
# Tynn.plugin(Tynn::Session, secret: "__change_me_not_secure__")
|
11
11
|
#
|
12
12
|
# Tynn.define do
|
13
|
-
# on
|
14
|
-
# post do
|
13
|
+
# on "login" do
|
14
|
+
# on post do
|
15
15
|
# # ...
|
16
16
|
#
|
17
17
|
# session[:user_id] = user.id
|
@@ -46,6 +46,14 @@ class Tynn
|
|
46
46
|
# If <tt>true</tt>, sets the <tt>Secure</tt> flag. This tells the browser
|
47
47
|
# to only transmit the cookie over HTTPS. Defaults to <tt>false</tt>.
|
48
48
|
#
|
49
|
+
# [same_site]
|
50
|
+
# Disables third-party usage for cookies. There are two possible values
|
51
|
+
# <tt>:Lax</tt> and <tt>:Strict</tt>. In <tt>Strict</tt> mode, the cookie
|
52
|
+
# is restrain to any cross-site usage; in <tt>Lax</tt> mode, some cross-site
|
53
|
+
# usage is allowed. Defaults to <tt>:Lax</tt>. If <tt>nil</tt> is passed,
|
54
|
+
# the flag is not included. Check this article[http://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/]
|
55
|
+
# for more information.
|
56
|
+
#
|
49
57
|
# [expire_after]
|
50
58
|
# The lifespan of the cookie. If <tt>nil</tt>, the session cookie is temporary
|
51
59
|
# and is no retained after the browser is closed. Defaults to <tt>nil</tt>.
|
@@ -58,7 +66,8 @@ class Tynn
|
|
58
66
|
# secret: ENV["SESSION_SECRET"],
|
59
67
|
# expire_after: 36_000, # seconds
|
60
68
|
# httponly: true,
|
61
|
-
# secure: true
|
69
|
+
# secure: true,
|
70
|
+
# same_site: :Strict
|
62
71
|
# )
|
63
72
|
#
|
64
73
|
module Session
|
@@ -68,7 +77,7 @@ class Tynn
|
|
68
77
|
secret = options[:secret]
|
69
78
|
|
70
79
|
if secret.nil?
|
71
|
-
raise <<~MSG
|
80
|
+
raise Tynn::Error, <<~MSG
|
72
81
|
No secret option provided to Tynn::Session.
|
73
82
|
|
74
83
|
Tynn::Session uses a secret token to sign the cookie data, thus
|
@@ -86,7 +95,7 @@ class Tynn
|
|
86
95
|
end
|
87
96
|
|
88
97
|
if secret.length < SECRET_MIN_LENGTH
|
89
|
-
raise <<~MSG
|
98
|
+
raise Tynn::Error, <<~MSG
|
90
99
|
The secret provided is shorter than the minimum length.
|
91
100
|
|
92
101
|
Make sure the secret is long and all random. You can generate a
|
data/lib/tynn/ssl.rb
CHANGED
@@ -13,21 +13,24 @@ class Tynn
|
|
13
13
|
# 3. Setting the <tt>secure</tt> flag on cookies. This tells the browser to
|
14
14
|
# only transmit them over HTTPS.
|
15
15
|
#
|
16
|
-
# You can configure HSTS passing
|
17
|
-
# are supported:
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# <tt>
|
16
|
+
# You can configure HSTS passing through the <tt>:hsts</tt> option.
|
17
|
+
# The following options are supported:
|
18
|
+
#
|
19
|
+
# [expires]
|
20
|
+
# The time, in seconds, that the browser access the site only by HTTPS.
|
21
|
+
# Defaults to 180 days.
|
22
|
+
#
|
23
|
+
# [subdomains]
|
24
|
+
# If this is <tt>true</tt>, the rule applies to all the site's subdomains as
|
25
|
+
# well. Defaults to <tt>true</tt>.
|
26
|
+
#
|
27
|
+
# [preload]
|
28
|
+
# A limitation of HSTS is that the initial request remains unprotected if it
|
29
|
+
# uses HTTP. The same applies to the first request after the activity period
|
30
|
+
# specified by <tt>max-age</tt>. Modern browsers implements a "HSTS preload
|
31
|
+
# list", which contains known sites supporting HSTS. If you would like to
|
32
|
+
# include your website into the list, set this option to <tt>true</tt> and
|
33
|
+
# submit your domain to this form[https://hstspreload.appspot.com/].
|
31
34
|
# Supported by Chrome, Firefox, IE11+ and IE Edge.
|
32
35
|
#
|
33
36
|
# To disable HSTS, you will need to tell the browser to expire it immediately.
|
@@ -74,7 +77,7 @@ class Tynn
|
|
74
77
|
end
|
75
78
|
|
76
79
|
def call(env)
|
77
|
-
request =
|
80
|
+
request = Tynn::Request.new(env)
|
78
81
|
|
79
82
|
return redirect_to_https(request) unless request.ssl?
|
80
83
|
|
data/lib/tynn/test.rb
CHANGED
@@ -7,16 +7,16 @@ class Tynn
|
|
7
7
|
# require "tynn/test"
|
8
8
|
#
|
9
9
|
# Tynn.define do
|
10
|
-
#
|
10
|
+
# on get do
|
11
11
|
# res.write("Hei!")
|
12
12
|
# end
|
13
13
|
# end
|
14
14
|
#
|
15
|
-
#
|
16
|
-
#
|
15
|
+
# ts = Tynn::Test.new
|
16
|
+
# ts.get("/")
|
17
17
|
#
|
18
|
-
#
|
19
|
-
#
|
18
|
+
# ts.res.status # => 200
|
19
|
+
# ts.res.body.join # => "Hei!"
|
20
20
|
#
|
21
21
|
class Test
|
22
22
|
attr_reader :app # :nodoc:
|
@@ -26,8 +26,8 @@ class Tynn
|
|
26
26
|
# class API < Tynn
|
27
27
|
# end
|
28
28
|
#
|
29
|
-
#
|
30
|
-
#
|
29
|
+
# ts = Tynn::Test.new(API)
|
30
|
+
# ts.get("/user.json")
|
31
31
|
#
|
32
32
|
def initialize(app = Tynn)
|
33
33
|
@app = app
|
@@ -52,40 +52,38 @@ class Tynn
|
|
52
52
|
# end
|
53
53
|
#
|
54
54
|
module Methods
|
55
|
-
# If a request has been issued, returns an instance of
|
56
|
-
# Rack::Request[http://www.rubydoc.info/gems/rack/Rack/Request].
|
55
|
+
# If a request has been issued, returns an instance of Tynn::Request.
|
57
56
|
# Otherwise, returns <tt>nil</tt>.
|
58
57
|
#
|
59
|
-
#
|
60
|
-
#
|
58
|
+
# ts = Tynn::Test.new
|
59
|
+
# ts.get("/", { foo: "foo" }, { "HTTP_USER_AGENT" => "Tynn::Test" })
|
61
60
|
#
|
62
|
-
#
|
61
|
+
# ts.req.get?
|
63
62
|
# # => true
|
64
63
|
#
|
65
|
-
#
|
64
|
+
# ts.req.params["foo"]
|
66
65
|
# # => "foo"
|
67
66
|
#
|
68
|
-
#
|
67
|
+
# ts.req.env["HTTP_USER_AGENT"]
|
69
68
|
# # => "Tynn::Test"
|
70
69
|
#
|
71
70
|
def req
|
72
71
|
@__req
|
73
72
|
end
|
74
73
|
|
75
|
-
# If a request has been issued, returns an instance of
|
76
|
-
# Rack::MockResponse[http://www.rubydoc.info/gems/rack/Rack/MockResponse].
|
74
|
+
# If a request has been issued, returns an instance of Tynn::Response
|
77
75
|
# Otherwise, returns <tt>nil</tt>.
|
78
76
|
#
|
79
|
-
#
|
80
|
-
#
|
77
|
+
# ts = Tynn::Test.new
|
78
|
+
# ts.get("/", name: "Jane")
|
81
79
|
#
|
82
|
-
#
|
80
|
+
# ts.res.status
|
83
81
|
# # => 200
|
84
82
|
#
|
85
|
-
#
|
83
|
+
# ts.res.body.join
|
86
84
|
# # => "Hello Jane!"
|
87
85
|
#
|
88
|
-
#
|
86
|
+
# ts.res.headers["Content-Type"]
|
89
87
|
# # => "text/html"
|
90
88
|
#
|
91
89
|
def res
|
@@ -99,9 +97,9 @@ class Tynn
|
|
99
97
|
# or <tt>nil</tt>.
|
100
98
|
# [env] A Hash of Rack environment values.
|
101
99
|
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
100
|
+
# ts = Tynn::Test.new
|
101
|
+
# ts.get("/search", name: "jane")
|
102
|
+
# ts.get("/cart", {}, { "HTTPS" => "on" })
|
105
103
|
#
|
106
104
|
def get(path, params = {}, env = {})
|
107
105
|
request(path, env.merge(method: "GET", params: params))
|
@@ -109,8 +107,8 @@ class Tynn
|
|
109
107
|
|
110
108
|
# Issues a <tt>POST</tt> request. See #get for more information.
|
111
109
|
#
|
112
|
-
#
|
113
|
-
#
|
110
|
+
# ts = Tynn::Test.new
|
111
|
+
# ts.post("/signup", username: "janedoe", password: "secret")
|
114
112
|
#
|
115
113
|
def post(path, params = {}, env = {})
|
116
114
|
request(path, env.merge(method: "POST", params: params))
|
@@ -118,8 +116,8 @@ class Tynn
|
|
118
116
|
|
119
117
|
# Issues a <tt>PUT</tt> request. See #get for more information.
|
120
118
|
#
|
121
|
-
#
|
122
|
-
#
|
119
|
+
# ts = Tynn::Test.new
|
120
|
+
# ts.put("/users/1", username: "johndoe", name: "John")
|
123
121
|
#
|
124
122
|
def put(path, params = {}, env = {})
|
125
123
|
request(path, env.merge(method: "PUT", params: params))
|
@@ -127,8 +125,8 @@ class Tynn
|
|
127
125
|
|
128
126
|
# Issues a <tt>PATCH</tt> request. See #get for more information.
|
129
127
|
#
|
130
|
-
#
|
131
|
-
#
|
128
|
+
# ts = Tynn::Test.new
|
129
|
+
# ts.patch("/users/1", username: "janedoe")
|
132
130
|
#
|
133
131
|
def patch(path, params = {}, env = {})
|
134
132
|
request(path, env.merge(method: "PATCH", params: params))
|
@@ -136,8 +134,8 @@ class Tynn
|
|
136
134
|
|
137
135
|
# Issues a <tt>DELETE</tt> request. See #get for more information.
|
138
136
|
#
|
139
|
-
#
|
140
|
-
#
|
137
|
+
# ts = Tynn::Test.new
|
138
|
+
# ts.delete("/users/1")
|
141
139
|
#
|
142
140
|
def delete(path, params = {}, env = {})
|
143
141
|
request(path, env.merge(method: "DELETE", params: params))
|
@@ -145,8 +143,8 @@ class Tynn
|
|
145
143
|
|
146
144
|
# Issues a <tt>HEAD</tt> request. See #get for more information.
|
147
145
|
#
|
148
|
-
#
|
149
|
-
#
|
146
|
+
# ts = Tynn::Test.new
|
147
|
+
# ts.head("/users/1")
|
150
148
|
#
|
151
149
|
def head(path, params = {}, env = {})
|
152
150
|
request(path, env.merge(method: Rack::HEAD, params: params))
|
@@ -154,8 +152,8 @@ class Tynn
|
|
154
152
|
|
155
153
|
# Issues a <tt>OPTIONS</tt> request. See #get for more information.
|
156
154
|
#
|
157
|
-
#
|
158
|
-
#
|
155
|
+
# ts = Tynn::Test.new
|
156
|
+
# ts.options("/users")
|
159
157
|
#
|
160
158
|
def options(path, params = {}, env = {})
|
161
159
|
request(path, env.merge(method: "OPTIONS", params: params))
|
@@ -164,8 +162,17 @@ class Tynn
|
|
164
162
|
private
|
165
163
|
|
166
164
|
def request(path, opts = {})
|
167
|
-
@__req =
|
168
|
-
@__res =
|
165
|
+
@__req = Tynn::Request.new(Rack::MockRequest.env_for(path, opts))
|
166
|
+
@__res = make_response(*app.call(@__req.env))
|
167
|
+
end
|
168
|
+
|
169
|
+
def make_response(status, headers, body)
|
170
|
+
res = Tynn::Response.new(headers)
|
171
|
+
res.status = status
|
172
|
+
|
173
|
+
body.each { |b| res.write(b) }
|
174
|
+
|
175
|
+
res
|
169
176
|
end
|
170
177
|
end
|
171
178
|
|
data/lib/tynn/utils.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Tynn
|
4
|
+
module Utils # :nodoc:
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def deepclone_hash(hash)
|
8
|
+
default_proc, hash.default_proc = hash.default_proc, nil
|
9
|
+
|
10
|
+
Marshal.load(Marshal.dump(hash))
|
11
|
+
ensure
|
12
|
+
hash.default_proc = default_proc
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/tynn/version.rb
CHANGED
data/test/environment_test.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -5,3 +5,12 @@ require "minitest/autorun"
|
|
5
5
|
require "minitest/pride"
|
6
6
|
require_relative "../lib/tynn"
|
7
7
|
require_relative "../lib/tynn/test"
|
8
|
+
|
9
|
+
class Minitest::Test
|
10
|
+
def new_app(parent: Tynn, lint: true)
|
11
|
+
app = Class.new(parent)
|
12
|
+
app.use(Rack::Lint) if lint
|
13
|
+
|
14
|
+
app
|
15
|
+
end
|
16
|
+
end
|
data/test/json_test.rb
CHANGED
@@ -5,14 +5,14 @@ require_relative "../lib/tynn/json"
|
|
5
5
|
|
6
6
|
class JSONTest < Minitest::Test
|
7
7
|
def setup
|
8
|
-
@app =
|
8
|
+
@app = new_app
|
9
9
|
end
|
10
10
|
|
11
11
|
def test_respond_json_object
|
12
12
|
@app.plugin(Tynn::JSON)
|
13
13
|
|
14
14
|
@app.define do
|
15
|
-
get do
|
15
|
+
on get do
|
16
16
|
json(foo: "foo")
|
17
17
|
end
|
18
18
|
end
|
@@ -20,7 +20,7 @@ class JSONTest < Minitest::Test
|
|
20
20
|
ts = Tynn::Test.new(@app)
|
21
21
|
ts.get("/")
|
22
22
|
|
23
|
-
object = JSON.parse(ts.res.body)
|
23
|
+
object = JSON.parse(ts.res.body.join)
|
24
24
|
|
25
25
|
assert_equal "foo", object["foo"]
|
26
26
|
end
|
@@ -29,7 +29,7 @@ class JSONTest < Minitest::Test
|
|
29
29
|
@app.plugin(Tynn::JSON)
|
30
30
|
|
31
31
|
@app.define do
|
32
|
-
get do
|
32
|
+
on get do
|
33
33
|
json(%w(foo bar baz))
|
34
34
|
end
|
35
35
|
end
|
@@ -37,14 +37,14 @@ class JSONTest < Minitest::Test
|
|
37
37
|
ts = Tynn::Test.new(@app)
|
38
38
|
ts.get("/")
|
39
39
|
|
40
|
-
assert_equal %w(foo bar baz), JSON.parse(ts.res.body)
|
40
|
+
assert_equal %w(foo bar baz), JSON.parse(ts.res.body.join)
|
41
41
|
end
|
42
42
|
|
43
43
|
def test_content_type
|
44
44
|
@app.plugin(Tynn::JSON)
|
45
45
|
|
46
46
|
@app.define do
|
47
|
-
get do
|
47
|
+
on get do
|
48
48
|
json(ok: true)
|
49
49
|
end
|
50
50
|
end
|
@@ -54,4 +54,19 @@ class JSONTest < Minitest::Test
|
|
54
54
|
|
55
55
|
assert_equal "application/json", ts.res.content_type
|
56
56
|
end
|
57
|
+
|
58
|
+
def test_custom_content_type
|
59
|
+
@app.plugin(Tynn::JSON, content_type: "application/js")
|
60
|
+
|
61
|
+
@app.define do
|
62
|
+
on get do
|
63
|
+
json(ok: true)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
ts = Tynn::Test.new(@app)
|
68
|
+
ts.get("/")
|
69
|
+
|
70
|
+
assert_equal "application/js", ts.res.content_type
|
71
|
+
end
|
57
72
|
end
|
data/test/middleware_test.rb
CHANGED
@@ -16,12 +16,12 @@ class MiddlewareTest < Minitest::Test
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_middleware_works
|
19
|
-
app =
|
19
|
+
app = new_app
|
20
20
|
|
21
21
|
app.use(Shrimp)
|
22
22
|
|
23
23
|
app.define do
|
24
|
-
get do
|
24
|
+
on get do
|
25
25
|
res.write("1")
|
26
26
|
res.write("2")
|
27
27
|
end
|
@@ -31,23 +31,23 @@ class MiddlewareTest < Minitest::Test
|
|
31
31
|
ts.get("/")
|
32
32
|
|
33
33
|
assert_equal 200, ts.res.status
|
34
|
-
assert_equal "21", ts.res.body
|
34
|
+
assert_equal "21", ts.res.body.join
|
35
35
|
end
|
36
36
|
|
37
37
|
def test_middleware_with_composition
|
38
|
-
app =
|
39
|
-
api =
|
38
|
+
app = new_app
|
39
|
+
api = new_app(lint: false)
|
40
40
|
|
41
41
|
app.use(Shrimp)
|
42
42
|
|
43
43
|
app.define do
|
44
|
-
on
|
44
|
+
on "api" do
|
45
45
|
run(api)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
api.define do
|
50
|
-
get do
|
50
|
+
on get do
|
51
51
|
res.write("1")
|
52
52
|
res.write("2")
|
53
53
|
end
|
@@ -57,23 +57,23 @@ class MiddlewareTest < Minitest::Test
|
|
57
57
|
ts.get("/api")
|
58
58
|
|
59
59
|
assert_equal 200, ts.res.status
|
60
|
-
assert_equal "21", ts.res.body
|
60
|
+
assert_equal "21", ts.res.body.join
|
61
61
|
end
|
62
62
|
|
63
63
|
def test_middleware_for_sub_application
|
64
|
-
app =
|
65
|
-
api =
|
64
|
+
app = new_app
|
65
|
+
api = new_app
|
66
66
|
|
67
67
|
api.use(Shrimp)
|
68
68
|
|
69
69
|
app.define do
|
70
|
-
on
|
70
|
+
on "api" do
|
71
71
|
run(api)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
75
|
api.define do
|
76
|
-
get do
|
76
|
+
on get do
|
77
77
|
res.write("1")
|
78
78
|
res.write("2")
|
79
79
|
end
|
@@ -83,6 +83,16 @@ class MiddlewareTest < Minitest::Test
|
|
83
83
|
ts.get("/api")
|
84
84
|
|
85
85
|
assert_equal 200, ts.res.status
|
86
|
-
assert_equal "21", ts.res.body
|
86
|
+
assert_equal "21", ts.res.body.join
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_raise_if_frozen
|
90
|
+
app = new_app
|
91
|
+
|
92
|
+
app.define {}
|
93
|
+
|
94
|
+
assert_raises(Tynn::Error) do
|
95
|
+
app.use(Shrimp, name: "foo", bar: 1)
|
96
|
+
end
|
87
97
|
end
|
88
98
|
end
|