tynn 1.4.0 → 2.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +540 -25
- data/lib/tynn.rb +50 -103
- data/lib/tynn/base.rb +97 -0
- data/lib/tynn/default_headers.rb +50 -0
- data/lib/tynn/environment.rb +54 -28
- data/lib/tynn/json.rb +7 -18
- data/lib/tynn/render.rb +16 -12
- data/lib/tynn/request.rb +54 -38
- data/lib/tynn/response.rb +33 -173
- data/lib/tynn/secure_headers.rb +28 -31
- data/lib/tynn/session.rb +68 -34
- data/lib/tynn/settings.rb +56 -27
- data/lib/tynn/ssl.rb +78 -70
- data/lib/tynn/static.rb +12 -21
- data/lib/tynn/test.rb +51 -78
- data/lib/tynn/version.rb +4 -7
- data/test/default_headers_test.rb +21 -9
- data/test/environment_test.rb +57 -16
- data/test/helper.rb +4 -6
- data/test/json_test.rb +48 -10
- data/test/middleware_test.rb +63 -54
- data/test/plugin_test.rb +121 -0
- data/test/render_test.rb +56 -65
- data/test/request_headers_test.rb +33 -0
- data/test/routing_test.rb +111 -0
- data/test/secure_headers_test.rb +29 -17
- data/test/session_test.rb +44 -11
- data/test/settings_test.rb +53 -0
- data/test/ssl_test.rb +107 -35
- data/test/static_test.rb +25 -6
- metadata +33 -38
- data/lib/tynn/all_methods.rb +0 -50
- data/lib/tynn/hmote.rb +0 -34
- data/lib/tynn/not_found.rb +0 -20
- data/lib/tynn/protection.rb +0 -45
- data/test/all_methods_test.rb +0 -16
- data/test/core_test.rb +0 -65
- data/test/hmote_test.rb +0 -78
- data/test/not_found_test.rb +0 -19
- data/test/protection_test.rb +0 -36
data/lib/tynn/json.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "json"
|
2
4
|
|
3
5
|
class Tynn
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# Examples
|
6
|
+
# Adds helper methods for JSON generation.
|
7
7
|
#
|
8
8
|
# require "tynn"
|
9
9
|
# require "tynn/json"
|
@@ -11,16 +11,9 @@ class Tynn
|
|
11
11
|
# Tynn.plugin(Tynn::JSON)
|
12
12
|
#
|
13
13
|
module JSON
|
14
|
-
CONTENT_TYPE = "application/json".freeze
|
15
|
-
|
16
14
|
module InstanceMethods
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# +Content-Type+ header to +application/json+.
|
20
|
-
#
|
21
|
-
# data - Any object that responds to +to_json+.
|
22
|
-
#
|
23
|
-
# Examples
|
15
|
+
# Generates a JSON document from <tt>data</tt> and writes it to the response body.
|
16
|
+
# It automatically sets the <tt>Content-Type</tt> header to <tt>application/json</tt>.
|
24
17
|
#
|
25
18
|
# Tynn.define do
|
26
19
|
# on("hash") do
|
@@ -30,16 +23,12 @@ class Tynn
|
|
30
23
|
# on("array") do
|
31
24
|
# json([1, 2, 3])
|
32
25
|
# end
|
33
|
-
#
|
34
|
-
# on("to_json") do
|
35
|
-
# json(Model.first)
|
36
|
-
# end
|
37
26
|
# end
|
38
27
|
#
|
39
28
|
def json(data)
|
40
|
-
res.headers[
|
29
|
+
res.headers["Content-Type"] = "application/json"
|
41
30
|
|
42
|
-
res.write(data
|
31
|
+
res.write(::JSON.generate(data))
|
43
32
|
end
|
44
33
|
end
|
45
34
|
end
|
data/lib/tynn/render.rb
CHANGED
@@ -1,48 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "tilt"
|
2
4
|
|
3
5
|
class Tynn
|
4
6
|
module Render
|
5
7
|
def self.setup(app, options = {}) # :nodoc:
|
6
|
-
app.
|
8
|
+
app.set(:render, {
|
7
9
|
layout: options.fetch(:layout, "layout"),
|
8
10
|
views: options.fetch(:views, File.expand_path("views", Dir.pwd)),
|
9
11
|
engine: options.fetch(:engine, "erb"),
|
10
12
|
engine_opts: {
|
11
13
|
escape_html: true
|
12
|
-
}.merge
|
13
|
-
)
|
14
|
+
}.merge(options.fetch(:options, {}))
|
15
|
+
})
|
14
16
|
end
|
15
17
|
|
16
18
|
module InstanceMethods
|
17
|
-
def render(template, locals = {}, layout = self.class.settings
|
19
|
+
def render(template, locals = {}, layout = self.class.settings.dig(:render, :layout))
|
18
20
|
res.headers[Rack::CONTENT_TYPE] ||= Syro::Response::DEFAULT
|
19
21
|
|
20
22
|
res.write(view(template, locals, layout))
|
21
23
|
end
|
22
24
|
|
23
|
-
def view(template, locals = {}, layout = self.class.settings
|
24
|
-
|
25
|
+
def view(template, locals = {}, layout = self.class.settings.dig(:render, :layout))
|
26
|
+
partial(layout, locals.merge(content: partial(template, locals)))
|
25
27
|
end
|
26
28
|
|
27
29
|
def partial(template, locals = {})
|
28
|
-
|
30
|
+
tilt(template_path(template), locals, self.class.settings.dig(:render, :engine_opts))
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
33
35
|
def tilt(file, locals = {}, opts = {})
|
34
|
-
|
36
|
+
tilt_cache.fetch(file) {
|
37
|
+
Tilt.new(file, 1, opts)
|
38
|
+
}.render(self, locals.merge(app: self))
|
35
39
|
end
|
36
40
|
|
37
41
|
def tilt_cache
|
38
|
-
|
42
|
+
Thread.current[:tilt_cache] ||= Tilt::Cache.new
|
39
43
|
end
|
40
44
|
|
41
45
|
def template_path(template)
|
42
|
-
dir = self.class.settings
|
43
|
-
ext = self.class.settings
|
46
|
+
dir = self.class.settings.dig(:render, :views)
|
47
|
+
ext = self.class.settings.dig(:render, :engine)
|
44
48
|
|
45
|
-
|
49
|
+
File.join(dir, "#{ template }.#{ ext }")
|
46
50
|
end
|
47
51
|
end
|
48
52
|
end
|
data/lib/tynn/request.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Tynn
|
2
|
-
#
|
3
|
-
# from a request.
|
4
|
-
#
|
5
|
-
# Examples
|
4
|
+
# It provides convenience methods for pulling out information from a request.
|
6
5
|
#
|
7
6
|
# env = {
|
8
|
-
# "REQUEST_METHOD" => "GET"
|
7
|
+
# "REQUEST_METHOD" => "GET",
|
9
8
|
# "QUERY_STRING" => "email=me@tynn.xyz"
|
10
9
|
# }
|
11
10
|
#
|
@@ -13,41 +12,58 @@ class Tynn
|
|
13
12
|
#
|
14
13
|
# req.get? # => true
|
15
14
|
# req.post? # => false
|
16
|
-
#
|
17
15
|
# req.params # => { "email" => "me@tynn.xyz" }
|
18
|
-
# req[:email] # => "me@tynn.xyz"
|
19
16
|
#
|
20
17
|
class Request < Rack::Request
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
18
|
+
class Headers
|
19
|
+
CGI_VARIABLES = Set.new(%w(
|
20
|
+
AUTH_TYPE
|
21
|
+
CONTENT_LENGTH
|
22
|
+
CONTENT_TYPE
|
23
|
+
GATEWAY_INTERFACE
|
24
|
+
HTTPS
|
25
|
+
PATH_INFO
|
26
|
+
PATH_TRANSLATED
|
27
|
+
QUERY_STRING
|
28
|
+
REMOTE_ADDR
|
29
|
+
REMOTE_HOST
|
30
|
+
REMOTE_IDENT
|
31
|
+
REMOTE_USER
|
32
|
+
REQUEST_METHOD
|
33
|
+
SCRIPT_NAME
|
34
|
+
SERVER_NAME
|
35
|
+
SERVER_PORT
|
36
|
+
SERVER_PROTOCOL
|
37
|
+
SERVER_SOFTWARE
|
38
|
+
)).freeze
|
39
|
+
|
40
|
+
def initialize(req)
|
41
|
+
@req = req
|
42
|
+
end
|
43
|
+
|
44
|
+
def [](key)
|
45
|
+
@req.env[transform_key(key)]
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch(key, *args, &block)
|
49
|
+
@req.env.fetch(transform_key(key), *args, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def key?(key)
|
53
|
+
@req.env.key?(transform_key(key))
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def transform_key(key)
|
59
|
+
key = key.upcase.tr("-", "_")
|
60
|
+
key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
|
61
|
+
key
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def headers
|
66
|
+
@headers ||= Headers.new(self)
|
67
|
+
end
|
52
68
|
end
|
53
69
|
end
|
data/lib/tynn/response.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Tynn
|
2
|
-
#
|
3
|
-
#
|
4
|
-
# Examples
|
4
|
+
# It provides convenience methods to construct a Rack response.
|
5
5
|
#
|
6
6
|
# res = Tynn::Response.new
|
7
7
|
#
|
@@ -13,185 +13,45 @@ class Tynn
|
|
13
13
|
# # => [200, { "Content-Type" => "text/html", "Content-Length" => 3 }, ["foo"]]
|
14
14
|
#
|
15
15
|
class Response < Syro::Response
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# headers - A Hash of initial headers. Defaults to <tt>{}</tt>.
|
19
|
-
#
|
20
|
-
# Examples
|
21
|
-
#
|
22
|
-
# Tynn::Response.new.headers
|
23
|
-
# # => {}
|
24
|
-
#
|
25
|
-
# Tynn::Response.new("Content-Type" => "text/plain").headers
|
26
|
-
# # => { "Content-Type" => "text/plain" }
|
27
|
-
#
|
28
|
-
# Signature
|
29
|
-
#
|
30
|
-
# new(headers = {})
|
31
|
-
#
|
32
|
-
# Inherited by Syro::Response.
|
33
|
-
|
34
|
-
# Public: Returns the response header corresponding to +key+.
|
35
|
-
#
|
36
|
-
# key - A String HTTP header field name.
|
37
|
-
#
|
38
|
-
# Examples
|
39
|
-
#
|
40
|
-
# res["Content-Type"] # => "text/html"
|
41
|
-
# res["Content-Length"] # => "42"
|
42
|
-
#
|
43
|
-
# Signature
|
44
|
-
#
|
45
|
-
# [](key)
|
46
|
-
#
|
47
|
-
# Inherited by Syro::Response.
|
48
|
-
|
49
|
-
# Public: Sets the given +value+ with the header corresponding to +key+.
|
50
|
-
#
|
51
|
-
# key - A String HTTP header field name.
|
52
|
-
# value - A String HTTP header field value.
|
53
|
-
#
|
54
|
-
# Examples
|
55
|
-
#
|
56
|
-
# res["Content-Type"] = "application/json"
|
57
|
-
# res["Content-Type"] # => "application/json"
|
58
|
-
#
|
59
|
-
# Signature
|
60
|
-
#
|
61
|
-
# []=(key, value)
|
62
|
-
#
|
63
|
-
# Inherited by Syro::Response.
|
64
|
-
|
65
|
-
# Public: Returns the body of the response.
|
66
|
-
#
|
67
|
-
# Examples
|
68
|
-
#
|
69
|
-
# res.body
|
70
|
-
# # => []
|
71
|
-
#
|
72
|
-
# res.write("there is")
|
73
|
-
# res.write("no try")
|
74
|
-
#
|
75
|
-
# res.body
|
76
|
-
# # => ["there is", "no try"]
|
77
|
-
#
|
78
|
-
# Signature
|
79
|
-
#
|
80
|
-
# body()
|
81
|
-
#
|
82
|
-
# Inherited by Syro::Response.
|
83
|
-
|
84
|
-
# Public: Returns an Array with three elements: the status, headers
|
85
|
-
# and body. If the status is not set, the status is set to +404+ if
|
86
|
-
# empty body, otherwise the status is set to +200+ and updates the
|
87
|
-
# +Content-Type+ header to +text/html+.
|
88
|
-
#
|
89
|
-
# Examples
|
90
|
-
#
|
91
|
-
# res.status = 200
|
92
|
-
# res.finish
|
93
|
-
# # => [200, {}, []]
|
16
|
+
# Sets a cookie into the response.
|
94
17
|
#
|
95
|
-
# res.
|
96
|
-
# res
|
97
|
-
# # => [404, {}, []]
|
18
|
+
# res.set_cookie("foo", "bar")
|
19
|
+
# res["Set-Cookie"] # => "foo=bar"
|
98
20
|
#
|
99
|
-
# res.
|
100
|
-
# res
|
101
|
-
# res.finish
|
102
|
-
# # => [200, { "Content-Type" => "text/html", "Content-Length" => 2 }, ["yo"]]
|
21
|
+
# res.set_cookie("foo2", "bar2")
|
22
|
+
# res["Set-Cookie"] # => "foo=bar\nfoo2=bar2"
|
103
23
|
#
|
104
|
-
#
|
24
|
+
# res.set_cookie("bar", "bar", {
|
25
|
+
# domain: ".example.com",
|
26
|
+
# path: "/",
|
27
|
+
# # max_age: 0,
|
28
|
+
# # expires: Time.now + 10_000,
|
29
|
+
# secure: true,
|
30
|
+
# httponly: true,
|
31
|
+
# same_site: :Lax
|
32
|
+
# })
|
105
33
|
#
|
106
|
-
#
|
34
|
+
# res["Set-Cookie"].split("\n").last
|
35
|
+
# # => "bar=bar; domain=.example.com; path=/; secure; HttpOnly; SameSite=Lax
|
107
36
|
#
|
108
|
-
#
|
109
|
-
|
110
|
-
# Public: Returns a Hash with the response headers.
|
111
|
-
#
|
112
|
-
# Examples
|
113
|
-
#
|
114
|
-
# res.headers
|
115
|
-
# # => { "Content-Type" => "text/html", "Content-Length" => "42" }
|
116
|
-
#
|
117
|
-
# Signature
|
118
|
-
#
|
119
|
-
# headers()
|
120
|
-
#
|
121
|
-
# Inherited by Syro::Response.
|
122
|
-
|
123
|
-
# Public: Sets the +Location+ header to +url+ and updates the status
|
124
|
-
# to +status+.
|
125
|
-
#
|
126
|
-
# url - A String URL (relative or absolute) to redirect to.
|
127
|
-
# status - An Integer status code. Defaults to +302+.
|
128
|
-
#
|
129
|
-
# Examples
|
130
|
-
#
|
131
|
-
# res.redirect("/path")
|
132
|
-
#
|
133
|
-
# res["Location"] # => "/path"
|
134
|
-
# res.status # => 302
|
135
|
-
#
|
136
|
-
# res.redirect("http://tynn.xyz", 303)
|
137
|
-
#
|
138
|
-
# res["Location"] # => "http://tynn.xyz"
|
139
|
-
# res.status # => 303
|
140
|
-
#
|
141
|
-
# Signature
|
142
|
-
#
|
143
|
-
# redirect(url, status = 302)
|
144
|
-
#
|
145
|
-
# Inherited by Syro::Response.
|
146
|
-
|
147
|
-
# Public: Returns the status of the response.
|
37
|
+
# *NOTE.* This method doesn't sign and/or encrypt the value of the cookie.
|
148
38
|
#
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
#
|
153
|
-
# Signature
|
154
|
-
#
|
155
|
-
# status()
|
156
|
-
#
|
157
|
-
# Inherited by Syro::Response.
|
39
|
+
def set_cookie(key, value, options = {})
|
40
|
+
Rack::Utils.set_cookie_header!(headers, key, options.merge(value: value))
|
41
|
+
end
|
158
42
|
|
159
|
-
#
|
160
|
-
#
|
161
|
-
# status - An Integer HTTP status code.
|
162
|
-
#
|
163
|
-
# Examples
|
164
|
-
#
|
165
|
-
# res.status = 200
|
166
|
-
#
|
167
|
-
# Signature
|
168
|
-
#
|
169
|
-
# status=(status)
|
170
|
-
#
|
171
|
-
# Inherited by Syro::Response.
|
172
|
-
|
173
|
-
# Public: Appends +str+ to the response body and updates the
|
174
|
-
# +Content-Length+ header.
|
175
|
-
#
|
176
|
-
# str - Any object that responds to +to_s+.
|
177
|
-
#
|
178
|
-
# Examples
|
179
|
-
#
|
180
|
-
# res.body # => []
|
181
|
-
#
|
182
|
-
# res.write("foo")
|
183
|
-
# res.write("bar")
|
184
|
-
#
|
185
|
-
# res.body
|
186
|
-
# # => ["foo", "bar"]
|
187
|
-
#
|
188
|
-
# res["Content-Length"]
|
189
|
-
# # => 6
|
43
|
+
# Deletes cookie by <tt>key</tt>.
|
190
44
|
#
|
191
|
-
#
|
45
|
+
# res.set_cookie("foo", "bar")
|
46
|
+
# res["Set-Cookie"]
|
47
|
+
# # => "foo=bar"
|
192
48
|
#
|
193
|
-
#
|
49
|
+
# res.delete_cookie("foo")
|
50
|
+
# res["Set-Cookie"]
|
51
|
+
# # => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
|
194
52
|
#
|
195
|
-
|
53
|
+
def delete_cookie(key, options = {})
|
54
|
+
Rack::Utils.delete_cookie_header!(headers, options)
|
55
|
+
end
|
196
56
|
end
|
197
57
|
end
|
data/lib/tynn/secure_headers.rb
CHANGED
@@ -1,44 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Tynn
|
2
|
-
#
|
4
|
+
# Adds the following security related HTTP headers:
|
3
5
|
#
|
4
|
-
#
|
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>.
|
5
9
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# Tynn.plugin(Tynn::SecureHeaders)
|
10
|
-
#
|
11
|
-
# This plugin applies the following headers:
|
12
|
-
#
|
13
|
-
# *X-Content-Type-Options:* <tt>"nosniff"</tt>
|
10
|
+
# [X-Frame-Options]
|
11
|
+
# Provides {Clickjacking}[https://www.owasp.org/index.php/Clickjacking]
|
12
|
+
# protection. Defaults to <tt>"SAMEORIGIN"</tt>.
|
14
13
|
#
|
15
|
-
#
|
16
|
-
#
|
14
|
+
# [X-Permitted-Cross-Domain-Policies]
|
15
|
+
# Restricts Adobe Flash Player's access to data. Defaults to <tt>"none"</tt>.
|
17
16
|
#
|
18
|
-
#
|
17
|
+
# [X-XSS-Protection]
|
18
|
+
# Enables the XSS protection filter built into IE, Chrome and Safari.
|
19
|
+
# This filter is usually enabled by default, the use of this header is to
|
20
|
+
# re-enable it if it was turned off by the user. Defaults to <tt>"1; mode=block"</tt>.
|
19
21
|
#
|
20
|
-
#
|
21
|
-
# protection.
|
22
|
+
# <tt></tt>
|
22
23
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# Restricts Adobe Flash Player's access to data.
|
26
|
-
#
|
27
|
-
# *X-XSS-Protection:* <tt>"1; mode=block"</tt>
|
24
|
+
# require "tynn"
|
25
|
+
# require "tynn/secure_headers"
|
28
26
|
#
|
29
|
-
#
|
30
|
-
# This filter is usually enabled by default, the use of this header
|
31
|
-
# is to re-enable it if it was turned off by the user.
|
27
|
+
# Tynn.plugin(Tynn::SecureHeaders)
|
32
28
|
#
|
33
29
|
module SecureHeaders
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
30
|
+
HEADERS = {
|
31
|
+
"X-Content-Type-Options" => "nosniff",
|
32
|
+
"X-Frame-Options" => "SAMEORIGIN",
|
33
|
+
"X-Permitted-Cross-Domain-Policies" => "none",
|
34
|
+
"X-XSS-Protection" => "1; mode=block"
|
35
|
+
}.freeze # :nodoc:
|
36
|
+
|
37
|
+
def self.setup(app) # :nodoc:
|
38
|
+
app.set!(:default_headers, HEADERS.merge(app.default_headers))
|
42
39
|
end
|
43
40
|
end
|
44
41
|
end
|