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