tynn 1.4.0 → 2.0.0.alpha
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 +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
|