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.
@@ -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 # :nodoc:
35
+ ).freeze
78
36
 
79
- def initialize(env) # :nodoc:
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
@@ -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
- # Initializes a new response object. An optional hash of HTTP headers can be
17
- # passed.
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
@@ -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 # :nodoc:
9
+ }.freeze
32
10
 
33
- def self.setup(app) # :nodoc:
11
+ def self.setup(app)
34
12
  app.set!(:default_headers, HEADERS.merge(app.default_headers))
35
13
  end
36
14
  end
@@ -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 # :nodoc:
8
+ SECRET_MIN_LENGTH = 30
78
9
 
79
- def self.setup(app, options = {}) # :nodoc:
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
- error: ArgumentError,
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