tynn 2.0.0.beta3 → 2.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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