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.
@@ -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>"SAMEORIGIN"</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" => "SAMEORIGIN",
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
 
@@ -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("login") do
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
@@ -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 a <tt>:hsts</tt> option. The following options
17
- # are supported:
18
- #
19
- # - *:expires* - The time, in seconds, that the browser access the site only
20
- # by HTTPS. Defaults to 180 days.
21
- #
22
- # - *:subdomains* - If this is <tt>true</tt>, the rule applies to all the
23
- # site's subdomains as well. Defaults to <tt>true</tt>.
24
- #
25
- # - *:preload* - A limitation of HSTS is that the initial request remains
26
- # unprotected if it uses HTTP. The same applies to the first request after
27
- # the activity period specified by <tt>max-age</tt>. Modern browsers implements
28
- # a "HSTS preload list", which contains known sites supporting HSTS. If you
29
- # would like to include your website into the list, set this option to
30
- # <tt>true</tt> and submit your domain to this form[https://hstspreload.appspot.com/].
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 = Rack::Request.new(env)
80
+ request = Tynn::Request.new(env)
78
81
 
79
82
  return redirect_to_https(request) unless request.ssl?
80
83
 
@@ -7,16 +7,16 @@ class Tynn
7
7
  # require "tynn/test"
8
8
  #
9
9
  # Tynn.define do
10
- # root do
10
+ # on get do
11
11
  # res.write("Hei!")
12
12
  # end
13
13
  # end
14
14
  #
15
- # app = Tynn::Test.new
16
- # app.get("/")
15
+ # ts = Tynn::Test.new
16
+ # ts.get("/")
17
17
  #
18
- # app.res.status # => 200
19
- # app.res.body # => "Hei!"
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
- # app = Tynn::Test.new(API)
30
- # app.get("/user.json")
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
- # app = Tynn::Test.new
60
- # app.get("/", { foo: "foo" }, { "HTTP_USER_AGENT" => "Tynn::Test" })
58
+ # ts = Tynn::Test.new
59
+ # ts.get("/", { foo: "foo" }, { "HTTP_USER_AGENT" => "Tynn::Test" })
61
60
  #
62
- # app.req.get?
61
+ # ts.req.get?
63
62
  # # => true
64
63
  #
65
- # app.req.params["foo"]
64
+ # ts.req.params["foo"]
66
65
  # # => "foo"
67
66
  #
68
- # app.req.env["HTTP_USER_AGENT"]
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
- # app = Tynn::Test.new
80
- # app.get("/", name: "Jane")
77
+ # ts = Tynn::Test.new
78
+ # ts.get("/", name: "Jane")
81
79
  #
82
- # app.res.status
80
+ # ts.res.status
83
81
  # # => 200
84
82
  #
85
- # app.res.body
83
+ # ts.res.body.join
86
84
  # # => "Hello Jane!"
87
85
  #
88
- # app.res["Content-Type"]
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
- # app = Tynn::Test.new
103
- # app.get("/search", name: "jane")
104
- # app.get("/cart", {}, { "HTTPS" => "on" })
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
- # app = Tynn::Test.new
113
- # app.post("/signup", username: "janedoe", password: "secret")
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
- # app = Tynn::Test.new
122
- # app.put("/users/1", username: "johndoe", name: "John")
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
- # app = Tynn::Test.new
131
- # app.patch("/users/1", username: "janedoe")
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
- # app = Tynn::Test.new
140
- # app.delete("/users/1")
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
- # app = Tynn::Test.new
149
- # app.head("/users/1")
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
- # app = Tynn::Test.new
158
- # app.options("/users")
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 = Rack::Request.new(Rack::MockRequest.env_for(path, opts))
168
- @__res = Rack::MockResponse.new(*app.call(@__req.env))
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tynn
4
- VERSION = "2.0.0.alpha" # :nodoc:
4
+ VERSION = "2.0.0.beta1" # :nodoc:
5
5
  end
@@ -4,7 +4,7 @@ require_relative "helper"
4
4
 
5
5
  class DefaultHeadersTest < Minitest::Test
6
6
  def setup
7
- @app = Class.new(Tynn)
7
+ @app = new_app
8
8
  end
9
9
 
10
10
  def test_set_and_get_headers
@@ -5,7 +5,7 @@ require_relative "../lib/tynn/environment"
5
5
 
6
6
  class EnvironmentTest < Minitest::Test
7
7
  def setup
8
- @app = Class.new(Tynn)
8
+ @app = new_app
9
9
  end
10
10
 
11
11
  def test_without_rack_env
@@ -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
@@ -5,14 +5,14 @@ require_relative "../lib/tynn/json"
5
5
 
6
6
  class JSONTest < Minitest::Test
7
7
  def setup
8
- @app = Class.new(Tynn)
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
@@ -16,12 +16,12 @@ class MiddlewareTest < Minitest::Test
16
16
  end
17
17
 
18
18
  def test_middleware_works
19
- app = Class.new(Tynn)
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 = Class.new(Tynn)
39
- api = Class.new(Tynn)
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("api") do
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 = Class.new(Tynn)
65
- api = Class.new(Tynn)
64
+ app = new_app
65
+ api = new_app
66
66
 
67
67
  api.use(Shrimp)
68
68
 
69
69
  app.define do
70
- on("api") do
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