goofy 1.0.2
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 +7 -0
- data/.gems +4 -0
- data/.gitignore +2 -0
- data/CHANGELOG +47 -0
- data/CONTRIBUTING +19 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +67 -0
- data/app/.rspec +2 -0
- data/app/Gemfile +13 -0
- data/app/app/controllers/application_controller.rb +3 -0
- data/app/app/services/.keep +0 -0
- data/app/config.ru +4 -0
- data/app/config/environment.rb +9 -0
- data/app/config/initializers/.keep +0 -0
- data/app/config/routes.rb +7 -0
- data/app/config/settings.rb +0 -0
- data/app/spec/helpers.rb +2 -0
- data/app/spec/helpers/goofy.rb +11 -0
- data/app/spec/spec_helper.rb +107 -0
- data/benchmark/measure.rb +35 -0
- data/bin/check_its_goofy.rb +15 -0
- data/bin/goofy +61 -0
- data/bin/goofy_generator.rb +357 -0
- data/bin/goofy_instance_creator.rb +40 -0
- data/examples/config.ru +18 -0
- data/examples/measure.rb +17 -0
- data/examples/rack-response.ru +21 -0
- data/examples/views/home.mote +7 -0
- data/examples/views/layout.mote +11 -0
- data/goofy.gemspec +26 -0
- data/lib/goofy.rb +405 -0
- data/lib/goofy/capybara.rb +13 -0
- data/lib/goofy/controller.rb +14 -0
- data/lib/goofy/controller/base.rb +21 -0
- data/lib/goofy/controller/callbacks.rb +19 -0
- data/lib/goofy/render.rb +63 -0
- data/lib/goofy/router.rb +9 -0
- data/lib/goofy/safe.rb +23 -0
- data/lib/goofy/safe/csrf.rb +47 -0
- data/lib/goofy/safe/secure_headers.rb +40 -0
- data/lib/goofy/test.rb +11 -0
- data/makefile +4 -0
- data/test/accept.rb +32 -0
- data/test/captures.rb +162 -0
- data/test/composition.rb +69 -0
- data/test/controller.rb +29 -0
- data/test/cookie.rb +34 -0
- data/test/csrf.rb +139 -0
- data/test/extension.rb +21 -0
- data/test/helper.rb +11 -0
- data/test/host.rb +29 -0
- data/test/integration.rb +114 -0
- data/test/match.rb +86 -0
- data/test/middleware.rb +46 -0
- data/test/number.rb +36 -0
- data/test/on.rb +157 -0
- data/test/param.rb +66 -0
- data/test/path.rb +86 -0
- data/test/plugin.rb +68 -0
- data/test/rack.rb +22 -0
- data/test/redirect.rb +21 -0
- data/test/render.rb +128 -0
- data/test/root.rb +83 -0
- data/test/run.rb +23 -0
- data/test/safe.rb +74 -0
- data/test/segment.rb +45 -0
- data/test/session.rb +21 -0
- data/test/settings.rb +52 -0
- data/test/views/about.erb +1 -0
- data/test/views/about.str +1 -0
- data/test/views/content-yield.erb +1 -0
- data/test/views/custom/abs_path.mote +1 -0
- data/test/views/frag.mote +1 -0
- data/test/views/home.erb +2 -0
- data/test/views/home.mote +1 -0
- data/test/views/home.str +2 -0
- data/test/views/layout-alternative.erb +2 -0
- data/test/views/layout-yield.erb +3 -0
- data/test/views/layout.erb +2 -0
- data/test/views/layout.mote +2 -0
- data/test/views/layout.str +2 -0
- data/test/views/test.erb +1 -0
- data/test/with.rb +42 -0
- metadata +271 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative "controller/callbacks"
|
2
|
+
require_relative "controller/base"
|
3
|
+
require_relative "router"
|
4
|
+
|
5
|
+
# include router functionality into Goofy
|
6
|
+
Goofy.include Goofy::Router
|
7
|
+
|
8
|
+
class Goofy
|
9
|
+
|
10
|
+
# Define Controller Class
|
11
|
+
class Controller
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Goofy
|
2
|
+
class Controller
|
3
|
+
|
4
|
+
def self.construct(arg)
|
5
|
+
self.new(arg).entry
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def response
|
13
|
+
res.write "<h3>You should define `response` instance method on your controller class!</h3>"
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(name, *args, &block)
|
17
|
+
@app.send(name, *args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'prong'
|
2
|
+
|
3
|
+
class Goofy
|
4
|
+
class Controller
|
5
|
+
|
6
|
+
include Prong
|
7
|
+
|
8
|
+
# Define before, around, after callbacks for #response
|
9
|
+
define_hook :response
|
10
|
+
|
11
|
+
def entry
|
12
|
+
run_hooks :response do
|
13
|
+
# Call response method with running callbacks
|
14
|
+
response
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/goofy/render.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "tilt"
|
2
|
+
|
3
|
+
class Goofy
|
4
|
+
module Render
|
5
|
+
def self.setup(app)
|
6
|
+
app.settings[:render] ||= {}
|
7
|
+
app.settings[:render][:template_engine] ||= "erb"
|
8
|
+
app.settings[:render][:layout] ||= "layout"
|
9
|
+
app.settings[:render][:views] ||= File.expand_path("views", Dir.pwd)
|
10
|
+
app.settings[:render][:options] ||= {
|
11
|
+
default_encoding: Encoding.default_external
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def render(template, locals = {}, layout = settings[:render][:layout])
|
16
|
+
res.headers["Content-Type"] ||= "text/html; charset=utf-8"
|
17
|
+
res.write(view(template, locals, layout))
|
18
|
+
end
|
19
|
+
|
20
|
+
def view(template, locals = {}, layout = settings[:render][:layout])
|
21
|
+
partial(layout, locals.merge(content: partial(template, locals)))
|
22
|
+
end
|
23
|
+
|
24
|
+
def partial(template, locals = {})
|
25
|
+
_render(template_path(template), locals, settings[:render][:options])
|
26
|
+
end
|
27
|
+
|
28
|
+
def template_path(template)
|
29
|
+
dir = settings[:render][:views]
|
30
|
+
ext = settings[:render][:template_engine]
|
31
|
+
|
32
|
+
return File.join(dir, "#{ template }.#{ ext }")
|
33
|
+
end
|
34
|
+
|
35
|
+
# @private Renders any type of template file supported by Tilt.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
#
|
39
|
+
# # Renders home, and is assumed to be HAML.
|
40
|
+
# _render("home.haml")
|
41
|
+
#
|
42
|
+
# # Renders with some local variables
|
43
|
+
# _render("home.haml", site_name: "My Site")
|
44
|
+
#
|
45
|
+
# # Renders with HAML options
|
46
|
+
# _render("home.haml", {}, ugly: true, format: :html5)
|
47
|
+
#
|
48
|
+
# # Renders in layout
|
49
|
+
# _render("layout.haml") { _render("home.haml") }
|
50
|
+
#
|
51
|
+
def _render(template, locals = {}, options = {}, &block)
|
52
|
+
_cache.fetch(template) {
|
53
|
+
Tilt.new(template, 1, options.merge(outvar: '@_output'))
|
54
|
+
}.render(self, locals, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @private Used internally by #_render to cache the
|
58
|
+
# Tilt templates.
|
59
|
+
def _cache
|
60
|
+
Thread.current[:_cache] ||= Tilt::Cache.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/goofy/router.rb
ADDED
data/lib/goofy/safe.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative "safe/csrf"
|
2
|
+
require_relative "safe/secure_headers"
|
3
|
+
|
4
|
+
class Goofy
|
5
|
+
# == Goofy::Safe
|
6
|
+
#
|
7
|
+
# This plugin contains security related features for Goofy
|
8
|
+
# applications. It takes ideas from secureheaders[1].
|
9
|
+
#
|
10
|
+
# == Usage
|
11
|
+
#
|
12
|
+
# require "goofy"
|
13
|
+
# require "goofy/safe"
|
14
|
+
#
|
15
|
+
# Goofy.plugin(Goofy::Safe)
|
16
|
+
#
|
17
|
+
module Safe
|
18
|
+
def self.setup(app)
|
19
|
+
app.plugin(Safe::SecureHeaders)
|
20
|
+
app.plugin(Safe::CSRF)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Goofy
|
2
|
+
module Safe
|
3
|
+
module CSRF
|
4
|
+
def csrf
|
5
|
+
@csrf ||= Goofy::Safe::CSRF::Helper.new(req)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Helper
|
9
|
+
attr :req
|
10
|
+
|
11
|
+
def initialize(req)
|
12
|
+
@req = req
|
13
|
+
end
|
14
|
+
|
15
|
+
def token
|
16
|
+
session[:csrf_token] ||= SecureRandom.base64(32)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset!
|
20
|
+
session.delete(:csrf_token)
|
21
|
+
end
|
22
|
+
|
23
|
+
def safe?
|
24
|
+
return req.get? || req.head? ||
|
25
|
+
req[:csrf_token] == token ||
|
26
|
+
req.env["HTTP_X_CSRF_TOKEN"] == token
|
27
|
+
end
|
28
|
+
|
29
|
+
def unsafe?
|
30
|
+
return !safe?
|
31
|
+
end
|
32
|
+
|
33
|
+
def form_tag
|
34
|
+
return %Q(<input type="hidden" name="csrf_token" value="#{ token }">)
|
35
|
+
end
|
36
|
+
|
37
|
+
def meta_tag
|
38
|
+
return %Q(<meta name="csrf_token" content="#{ token }">)
|
39
|
+
end
|
40
|
+
|
41
|
+
def session
|
42
|
+
return req.env["rack.session"]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# == Secure HTTP Headers
|
2
|
+
#
|
3
|
+
# This plugin will automatically apply several headers that are
|
4
|
+
# related to security. This includes:
|
5
|
+
#
|
6
|
+
# - HTTP Strict Transport Security (HSTS) [2].
|
7
|
+
# - X-Frame-Options [3].
|
8
|
+
# - X-XSS-Protection [4].
|
9
|
+
# - X-Content-Type-Options [5].
|
10
|
+
# - X-Download-Options [6].
|
11
|
+
# - X-Permitted-Cross-Domain-Policies [7].
|
12
|
+
#
|
13
|
+
# == References
|
14
|
+
#
|
15
|
+
# [1]: https://github.com/twitter/secureheaders
|
16
|
+
# [2]: https://tools.ietf.org/html/rfc6797
|
17
|
+
# [3]: https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02
|
18
|
+
# [4]: http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx
|
19
|
+
# [5]: http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
|
20
|
+
# [6]: http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx
|
21
|
+
# [7]: https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
|
22
|
+
#
|
23
|
+
class Goofy
|
24
|
+
module Safe
|
25
|
+
module SecureHeaders
|
26
|
+
HEADERS = {
|
27
|
+
"X-Content-Type-Options" => "nosniff",
|
28
|
+
"X-Download-Options" => "noopen",
|
29
|
+
"X-Frame-Options" => "SAMEORIGIN",
|
30
|
+
"X-Permitted-Cross-Domain-Policies" => "none",
|
31
|
+
"X-XSS-Protection" => "1; mode=block",
|
32
|
+
"Strict-Transport-Security" => "max-age=2628000"
|
33
|
+
}
|
34
|
+
|
35
|
+
def self.setup(app)
|
36
|
+
app.settings[:default_headers].merge!(HEADERS)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/goofy/test.rb
ADDED
data/makefile
ADDED
data/test/accept.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
test "accept mimetypes" do
|
4
|
+
Goofy.define do
|
5
|
+
on accept("application/xml") do
|
6
|
+
res.write res["Content-Type"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
env = { "HTTP_ACCEPT" => "application/xml",
|
11
|
+
"SCRIPT_NAME" => "/", "PATH_INFO" => "/post" }
|
12
|
+
|
13
|
+
_, _, body = Goofy.call(env)
|
14
|
+
|
15
|
+
assert_response body, ["application/xml"]
|
16
|
+
end
|
17
|
+
|
18
|
+
test "tests don't fail when you don't specify an accept type" do
|
19
|
+
Goofy.define do
|
20
|
+
on accept("application/xml") do
|
21
|
+
res.write res["Content-Type"]
|
22
|
+
end
|
23
|
+
|
24
|
+
on default do
|
25
|
+
res.write "Default action"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
_, _, body = Goofy.call({})
|
30
|
+
|
31
|
+
assert_response body, ["Default action"]
|
32
|
+
end
|
data/test/captures.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
test "doesn't yield HOST" do
|
4
|
+
Goofy.define do
|
5
|
+
on host("example.com") do |*args|
|
6
|
+
res.write args.size
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
env = { "HTTP_HOST" => "example.com" }
|
11
|
+
|
12
|
+
_, _, resp = Goofy.call(env)
|
13
|
+
|
14
|
+
assert_response resp, ["0"]
|
15
|
+
end
|
16
|
+
|
17
|
+
test "doesn't yield the verb" do
|
18
|
+
Goofy.define do
|
19
|
+
on get do |*args|
|
20
|
+
res.write args.size
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
env = { "REQUEST_METHOD" => "GET" }
|
25
|
+
|
26
|
+
_, _, resp = Goofy.call(env)
|
27
|
+
|
28
|
+
assert_response resp, ["0"]
|
29
|
+
end
|
30
|
+
|
31
|
+
test "doesn't yield the path" do
|
32
|
+
Goofy.define do
|
33
|
+
on get, "home" do |*args|
|
34
|
+
res.write args.size
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/home",
|
39
|
+
"SCRIPT_NAME" => "/" }
|
40
|
+
|
41
|
+
_, _, resp = Goofy.call(env)
|
42
|
+
|
43
|
+
assert_response resp, ["0"]
|
44
|
+
end
|
45
|
+
|
46
|
+
test "yields the segment" do
|
47
|
+
Goofy.define do
|
48
|
+
on get, "user", :id do |id|
|
49
|
+
res.write id
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/johndoe",
|
54
|
+
"SCRIPT_NAME" => "/" }
|
55
|
+
|
56
|
+
_, _, resp = Goofy.call(env)
|
57
|
+
|
58
|
+
assert_response resp, ["johndoe"]
|
59
|
+
end
|
60
|
+
|
61
|
+
test "yields a number" do
|
62
|
+
Goofy.define do
|
63
|
+
on get, "user", :id do |id|
|
64
|
+
res.write id
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/user/101",
|
69
|
+
"SCRIPT_NAME" => "/" }
|
70
|
+
|
71
|
+
_, _, resp = Goofy.call(env)
|
72
|
+
|
73
|
+
assert_response resp, ["101"]
|
74
|
+
end
|
75
|
+
|
76
|
+
test "yield a file name with a matching extension" do
|
77
|
+
Goofy.define do
|
78
|
+
on get, "css", extension("css") do |file|
|
79
|
+
res.write file
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/css/app.css",
|
84
|
+
"SCRIPT_NAME" => "/" }
|
85
|
+
|
86
|
+
_, _, resp = Goofy.call(env)
|
87
|
+
|
88
|
+
assert_response resp, ["app"]
|
89
|
+
end
|
90
|
+
|
91
|
+
test "yields a segment per nested block" do
|
92
|
+
Goofy.define do
|
93
|
+
on :one do |one|
|
94
|
+
on :two do |two|
|
95
|
+
on :three do |three|
|
96
|
+
res.write one
|
97
|
+
res.write two
|
98
|
+
res.write three
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/one/two/three",
|
105
|
+
"SCRIPT_NAME" => "/" }
|
106
|
+
|
107
|
+
_, _, resp = Goofy.call(env)
|
108
|
+
|
109
|
+
assert_response resp, ["one", "two", "three"]
|
110
|
+
end
|
111
|
+
|
112
|
+
test "consumes a slash if needed" do
|
113
|
+
Goofy.define do
|
114
|
+
on get, "(.+\\.css)" do |file|
|
115
|
+
res.write file
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/foo/bar.css",
|
120
|
+
"SCRIPT_NAME" => "/" }
|
121
|
+
|
122
|
+
_, _, resp = Goofy.call(env)
|
123
|
+
|
124
|
+
assert_response resp, ["foo/bar.css"]
|
125
|
+
end
|
126
|
+
|
127
|
+
test "regex captures in string format" do
|
128
|
+
Goofy.define do
|
129
|
+
on get, "posts/(\\d+)-(.*)" do |id, slug|
|
130
|
+
res.write id
|
131
|
+
res.write slug
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
env = { "REQUEST_METHOD" => "GET",
|
137
|
+
"PATH_INFO" => "/posts/123-postal-service",
|
138
|
+
"SCRIPT_NAME" => "/" }
|
139
|
+
|
140
|
+
_, _, resp = Goofy.call(env)
|
141
|
+
|
142
|
+
|
143
|
+
assert_response resp, ["123", "postal-service"]
|
144
|
+
end
|
145
|
+
|
146
|
+
test "regex captures in regex format" do
|
147
|
+
Goofy.define do
|
148
|
+
on get, %r{posts/(\d+)-(.*)} do |id, slug|
|
149
|
+
res.write id
|
150
|
+
res.write slug
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
env = { "REQUEST_METHOD" => "GET",
|
155
|
+
"PATH_INFO" => "/posts/123-postal-service",
|
156
|
+
"SCRIPT_NAME" => "/" }
|
157
|
+
|
158
|
+
_, _, resp = Goofy.call(env)
|
159
|
+
|
160
|
+
|
161
|
+
assert_response resp, ["123", "postal-service"]
|
162
|
+
end
|