roda 3.83.0 → 3.85.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/roda/plugins/Integer_matcher_max.rb +9 -8
- data/lib/roda/plugins/_optimized_matching.rb +2 -2
- data/lib/roda/plugins/_symbol_class_matchers.rb +107 -0
- data/lib/roda/plugins/capture_erb.rb +6 -5
- data/lib/roda/plugins/class_matchers.rb +91 -14
- data/lib/roda/plugins/hsts.rb +35 -0
- data/lib/roda/plugins/placeholder_string_matchers.rb +4 -0
- data/lib/roda/plugins/public.rb +1 -1
- data/lib/roda/plugins/render.rb +1 -1
- data/lib/roda/plugins/symbol_matchers.rb +70 -15
- data/lib/roda/request.rb +16 -13
- data/lib/roda/response.rb +1 -1
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +7 -0
- metadata +5 -179
- data/CHANGELOG +0 -691
- data/README.rdoc +0 -1136
- data/doc/conventions.rdoc +0 -177
- data/doc/release_notes/3.0.0.txt +0 -84
- data/doc/release_notes/3.1.0.txt +0 -24
- data/doc/release_notes/3.10.0.txt +0 -132
- data/doc/release_notes/3.11.0.txt +0 -54
- data/doc/release_notes/3.12.0.txt +0 -19
- data/doc/release_notes/3.13.0.txt +0 -38
- data/doc/release_notes/3.14.0.txt +0 -36
- data/doc/release_notes/3.14.1.txt +0 -43
- data/doc/release_notes/3.15.0.txt +0 -21
- data/doc/release_notes/3.16.0.txt +0 -52
- data/doc/release_notes/3.17.0.txt +0 -62
- data/doc/release_notes/3.18.0.txt +0 -170
- data/doc/release_notes/3.19.0.txt +0 -229
- data/doc/release_notes/3.2.0.txt +0 -22
- data/doc/release_notes/3.20.0.txt +0 -7
- data/doc/release_notes/3.21.0.txt +0 -5
- data/doc/release_notes/3.22.0.txt +0 -24
- data/doc/release_notes/3.23.0.txt +0 -28
- data/doc/release_notes/3.24.0.txt +0 -14
- data/doc/release_notes/3.25.0.txt +0 -12
- data/doc/release_notes/3.26.0.txt +0 -15
- data/doc/release_notes/3.27.0.txt +0 -15
- data/doc/release_notes/3.28.0.txt +0 -13
- data/doc/release_notes/3.29.0.txt +0 -15
- data/doc/release_notes/3.3.0.txt +0 -291
- data/doc/release_notes/3.30.0.txt +0 -14
- data/doc/release_notes/3.31.0.txt +0 -11
- data/doc/release_notes/3.32.0.txt +0 -42
- data/doc/release_notes/3.33.0.txt +0 -8
- data/doc/release_notes/3.34.0.txt +0 -17
- data/doc/release_notes/3.35.0.txt +0 -12
- data/doc/release_notes/3.36.0.txt +0 -17
- data/doc/release_notes/3.37.0.txt +0 -42
- data/doc/release_notes/3.38.0.txt +0 -5
- data/doc/release_notes/3.39.0.txt +0 -16
- data/doc/release_notes/3.4.0.txt +0 -24
- data/doc/release_notes/3.40.0.txt +0 -24
- data/doc/release_notes/3.41.0.txt +0 -9
- data/doc/release_notes/3.42.0.txt +0 -21
- data/doc/release_notes/3.43.0.txt +0 -34
- data/doc/release_notes/3.44.0.txt +0 -23
- data/doc/release_notes/3.45.0.txt +0 -22
- data/doc/release_notes/3.46.0.txt +0 -19
- data/doc/release_notes/3.47.0.txt +0 -13
- data/doc/release_notes/3.48.0.txt +0 -10
- data/doc/release_notes/3.49.0.txt +0 -18
- data/doc/release_notes/3.5.0.txt +0 -31
- data/doc/release_notes/3.50.0.txt +0 -21
- data/doc/release_notes/3.51.0.txt +0 -20
- data/doc/release_notes/3.52.0.txt +0 -20
- data/doc/release_notes/3.53.0.txt +0 -14
- data/doc/release_notes/3.54.0.txt +0 -48
- data/doc/release_notes/3.55.0.txt +0 -12
- data/doc/release_notes/3.56.0.txt +0 -33
- data/doc/release_notes/3.57.0.txt +0 -34
- data/doc/release_notes/3.58.0.txt +0 -16
- data/doc/release_notes/3.59.0.txt +0 -17
- data/doc/release_notes/3.6.0.txt +0 -21
- data/doc/release_notes/3.60.0.txt +0 -56
- data/doc/release_notes/3.61.0.txt +0 -24
- data/doc/release_notes/3.62.0.txt +0 -41
- data/doc/release_notes/3.63.0.txt +0 -36
- data/doc/release_notes/3.64.0.txt +0 -26
- data/doc/release_notes/3.65.0.txt +0 -12
- data/doc/release_notes/3.66.0.txt +0 -23
- data/doc/release_notes/3.67.0.txt +0 -25
- data/doc/release_notes/3.68.0.txt +0 -21
- data/doc/release_notes/3.69.0.txt +0 -33
- data/doc/release_notes/3.7.0.txt +0 -123
- data/doc/release_notes/3.70.0.txt +0 -19
- data/doc/release_notes/3.71.0.txt +0 -33
- data/doc/release_notes/3.72.0.txt +0 -48
- data/doc/release_notes/3.73.0.txt +0 -33
- data/doc/release_notes/3.74.0.txt +0 -28
- data/doc/release_notes/3.75.0.txt +0 -19
- data/doc/release_notes/3.76.0.txt +0 -18
- data/doc/release_notes/3.77.0.txt +0 -8
- data/doc/release_notes/3.78.0.txt +0 -99
- data/doc/release_notes/3.79.0.txt +0 -148
- data/doc/release_notes/3.8.0.txt +0 -27
- data/doc/release_notes/3.80.0.txt +0 -31
- data/doc/release_notes/3.81.0.txt +0 -24
- data/doc/release_notes/3.82.0.txt +0 -43
- data/doc/release_notes/3.83.0.txt +0 -6
- data/doc/release_notes/3.9.0.txt +0 -67
data/README.rdoc
DELETED
@@ -1,1136 +0,0 @@
|
|
1
|
-
rdoc-image:https://roda.jeremyevans.net/images/roda-logo.svg
|
2
|
-
|
3
|
-
A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby.
|
4
|
-
|
5
|
-
== Table of contents
|
6
|
-
|
7
|
-
- {Installation}[#label-Installation]
|
8
|
-
- {Resources}[#label-Resources]
|
9
|
-
- {Goals}[#label-Goals]
|
10
|
-
- {Usage}[#label-Usage]
|
11
|
-
- {Running the application}[#label-Running+the+Application]
|
12
|
-
- {The routing tree}[#label-The+Routing+Tree]
|
13
|
-
- {Matchers}[#label-Matchers]
|
14
|
-
- {Optional segments}[#label-Optional+segments]
|
15
|
-
- {Match/Route Block Return Values}[#label-Match-2FRoute+Block+Return+Values]
|
16
|
-
- {Status codes}[#label-Status+Codes]
|
17
|
-
- {Verb methods}[#label-Verb+Methods]
|
18
|
-
- {Root method}[#label-Root+Method]
|
19
|
-
- {Request and Response}[#label-Request+and+Response]
|
20
|
-
- {Pollution}[#label-Pollution]
|
21
|
-
- {Composition}[#label-Composition]
|
22
|
-
- {Testing}[#label-Testing]
|
23
|
-
- {Settings}[#label-Settings]
|
24
|
-
- {Rendering}[#label-Rendering]
|
25
|
-
- {Security}[#label-Security]
|
26
|
-
- {Code Reloading}[#label-Code+Reloading]
|
27
|
-
- {Plugins}[#label-Plugins]
|
28
|
-
- {No introspection}[#label-No+Introspection]
|
29
|
-
- {Inspiration}[#label-Inspiration]
|
30
|
-
- {Ruby Support Policy}[#label-Ruby+Support+Policy]
|
31
|
-
|
32
|
-
== Installation
|
33
|
-
|
34
|
-
$ gem install roda
|
35
|
-
|
36
|
-
== Resources
|
37
|
-
|
38
|
-
Website :: http://roda.jeremyevans.net
|
39
|
-
Source :: http://github.com/jeremyevans/roda
|
40
|
-
Bugs :: http://github.com/jeremyevans/roda/issues
|
41
|
-
Discussion Forum (GitHub Discussions) :: https://github.com/jeremyevans/roda/discussions
|
42
|
-
Alternate Discussion Forum (Google Group) :: http://groups.google.com/group/ruby-roda
|
43
|
-
|
44
|
-
== Goals
|
45
|
-
|
46
|
-
* Simplicity
|
47
|
-
* Reliability
|
48
|
-
* Extensibility
|
49
|
-
* Performance
|
50
|
-
|
51
|
-
=== Simplicity
|
52
|
-
|
53
|
-
Roda is designed to be simple, both internally and externally.
|
54
|
-
It uses a routing tree to enable you to write simpler and DRYer
|
55
|
-
code.
|
56
|
-
|
57
|
-
=== Reliability
|
58
|
-
|
59
|
-
Roda supports and encourages immutability. Roda apps are designed
|
60
|
-
to be frozen in production, which eliminates possible thread safety issues.
|
61
|
-
Additionally, Roda limits the instance variables, constants, and
|
62
|
-
methods that it uses, so that they do not conflict with the ones
|
63
|
-
you use for your application.
|
64
|
-
|
65
|
-
=== Extensibility
|
66
|
-
|
67
|
-
Roda is built completely out of plugins, which makes it very
|
68
|
-
extensible. You can override any part of Roda and call super
|
69
|
-
to get the default behavior.
|
70
|
-
|
71
|
-
=== Performance
|
72
|
-
|
73
|
-
Roda has low per-request overhead, and the use of a routing tree
|
74
|
-
and intelligent caching of internal datastructures makes it
|
75
|
-
significantly faster than other popular ruby web frameworks.
|
76
|
-
|
77
|
-
== Usage
|
78
|
-
|
79
|
-
Here's a simple application, showing how the routing tree works:
|
80
|
-
|
81
|
-
# cat config.ru
|
82
|
-
require "roda"
|
83
|
-
|
84
|
-
class App < Roda
|
85
|
-
route do |r|
|
86
|
-
# GET / request
|
87
|
-
r.root do
|
88
|
-
r.redirect "/hello"
|
89
|
-
end
|
90
|
-
|
91
|
-
# /hello branch
|
92
|
-
r.on "hello" do
|
93
|
-
# Set variable for all routes in /hello branch
|
94
|
-
@greeting = 'Hello'
|
95
|
-
|
96
|
-
# GET /hello/world request
|
97
|
-
r.get "world" do
|
98
|
-
"#{@greeting} world!"
|
99
|
-
end
|
100
|
-
|
101
|
-
# /hello request
|
102
|
-
r.is do
|
103
|
-
# GET /hello request
|
104
|
-
r.get do
|
105
|
-
"#{@greeting}!"
|
106
|
-
end
|
107
|
-
|
108
|
-
# POST /hello request
|
109
|
-
r.post do
|
110
|
-
puts "Someone said #{@greeting}!"
|
111
|
-
r.redirect
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
run App.freeze.app
|
119
|
-
|
120
|
-
Here's a breakdown of what is going on in the block above:
|
121
|
-
|
122
|
-
The +route+ block is called whenever a new request comes in.
|
123
|
-
It is yielded an instance of a subclass of <tt>Rack::Request</tt>
|
124
|
-
with some additional methods for matching routes.
|
125
|
-
By convention, this argument should be named +r+.
|
126
|
-
|
127
|
-
The primary way routes are matched in Roda is by calling
|
128
|
-
+r.on+, +r.is+, +r.root+, +r.get+, or +r.post+.
|
129
|
-
Each of these "routing methods" takes a "match block".
|
130
|
-
|
131
|
-
Each routing method takes each of the arguments (called matchers)
|
132
|
-
that are given and tries to match it to the current request.
|
133
|
-
If the method is able to match all of the arguments, it yields to the match block;
|
134
|
-
otherwise, the block is skipped and execution continues.
|
135
|
-
|
136
|
-
- +r.on+ matches if all of the arguments match.
|
137
|
-
- +r.is+ matches if all of the arguments match and there are no
|
138
|
-
further entries in the path after matching.
|
139
|
-
- +r.get+ matches any +GET+ request when called without arguments.
|
140
|
-
- +r.get+ (when called with any arguments) matches only if the
|
141
|
-
current request is a +GET+ request and there are no further entries
|
142
|
-
in the path after matching.
|
143
|
-
- +r.root+ only matches a +GET+ request where the current path is +/+.
|
144
|
-
|
145
|
-
If a routing method matches and control is yielded to the match block,
|
146
|
-
whenever the match block returns, Roda will return the Rack response array
|
147
|
-
(containing status, headers, and body) to the caller.
|
148
|
-
|
149
|
-
If the match block returns a string
|
150
|
-
and the response body hasn't already been written to,
|
151
|
-
the block return value will be interpreted as the body for the response.
|
152
|
-
If none of the routing methods match and the route block returns a string,
|
153
|
-
it will be interpreted as the body for the response.
|
154
|
-
|
155
|
-
+r.redirect+ immediately returns the response,
|
156
|
-
allowing for code such as <tt>r.redirect(path) if some_condition</tt>.
|
157
|
-
If +r.redirect+ is called without arguments
|
158
|
-
and the current request method is not +GET+, it redirects to the current path.
|
159
|
-
|
160
|
-
The +.freeze.app+ at the end is optional. Freezing the app makes modifying
|
161
|
-
app-level settings raise an error, alerting you to possible thread-safety issues
|
162
|
-
in your application. It is recommended to freeze the app in production and
|
163
|
-
during testing. The +.app+ is an optimization, which saves a few method calls
|
164
|
-
for every request.
|
165
|
-
|
166
|
-
== Running the Application
|
167
|
-
|
168
|
-
Running a Roda application is similar to running any other rack-based application
|
169
|
-
that uses a +config.ru+ file. You can start a basic server using +rackup+, +puma+,
|
170
|
-
+unicorn+, +passenger+, or any other webserver that can handle +config.ru+ files:
|
171
|
-
|
172
|
-
$ rackup
|
173
|
-
|
174
|
-
== The Routing Tree
|
175
|
-
|
176
|
-
Roda is called a routing tree web toolkit because the way most sites are structured,
|
177
|
-
routing takes the form of a tree (based on the URL structure of the site).
|
178
|
-
In general:
|
179
|
-
|
180
|
-
- +r.on+ is used to split the tree into different branches.
|
181
|
-
- +r.is+ finalizes the routing path.
|
182
|
-
- +r.get+ and +r.post+ handle specific request methods.
|
183
|
-
|
184
|
-
So, a simple routing tree might look something like this:
|
185
|
-
|
186
|
-
r.on "a" do # /a branch
|
187
|
-
r.on "b" do # /a/b branch
|
188
|
-
r.is "c" do # /a/b/c request
|
189
|
-
r.get do end # GET /a/b/c request
|
190
|
-
r.post do end # POST /a/b/c request
|
191
|
-
end
|
192
|
-
r.get "d" do end # GET /a/b/d request
|
193
|
-
r.post "e" do end # POST /a/b/e request
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
It's also possible to handle the same requests,
|
198
|
-
but structure the routing tree by first branching on the request method:
|
199
|
-
|
200
|
-
r.get do # GET
|
201
|
-
r.on "a" do # GET /a branch
|
202
|
-
r.on "b" do # GET /a/b branch
|
203
|
-
r.is "c" do end # GET /a/b/c request
|
204
|
-
r.is "d" do end # GET /a/b/d request
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
r.post do # POST
|
210
|
-
r.on "a" do # POST /a branch
|
211
|
-
r.on "b" do # POST /a/b branch
|
212
|
-
r.is "c" do end # POST /a/b/c request
|
213
|
-
r.is "e" do end # POST /a/b/e request
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
This allows you to easily separate your +GET+ request handling
|
219
|
-
from your +POST+ request handling.
|
220
|
-
If you only have a small number of +POST+ request URLs
|
221
|
-
and a large number of +GET+ request URLs, this may make things easier.
|
222
|
-
|
223
|
-
However, routing first by the path and last by the request method
|
224
|
-
is likely to lead to simpler and DRYer code.
|
225
|
-
This is because you can act on the request at any point during the routing.
|
226
|
-
For example, if all requests in the +/a+ branch need access permission +A+
|
227
|
-
and all requests in the +/a/b+ branch need access permission +B+,
|
228
|
-
you can easily handle this in the routing tree:
|
229
|
-
|
230
|
-
r.on "a" do # /a branch
|
231
|
-
check_perm(:A)
|
232
|
-
r.on "b" do # /a/b branch
|
233
|
-
check_perm(:B)
|
234
|
-
r.is "c" do # /a/b/c request
|
235
|
-
r.get do end # GET /a/b/c request
|
236
|
-
r.post do end # POST /a/b/c request
|
237
|
-
end
|
238
|
-
r.get "d" do end # GET /a/b/d request
|
239
|
-
r.post "e" do end # POST /a/b/e request
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
Being able to operate on the request at any point during the routing
|
244
|
-
is one of the major advantages of Roda.
|
245
|
-
|
246
|
-
== Matchers
|
247
|
-
|
248
|
-
Other than +r.root+, the routing methods all take arguments called matchers.
|
249
|
-
If all of the matchers match, the routing method yields to the match block.
|
250
|
-
Here's an example showcasing how different matchers work:
|
251
|
-
|
252
|
-
class App < Roda
|
253
|
-
route do |r|
|
254
|
-
# GET /
|
255
|
-
r.root do
|
256
|
-
"Home"
|
257
|
-
end
|
258
|
-
|
259
|
-
# GET /about
|
260
|
-
r.get "about" do
|
261
|
-
"About"
|
262
|
-
end
|
263
|
-
|
264
|
-
# GET /post/2011/02/16/hello
|
265
|
-
r.get "post", Integer, Integer, Integer, String do |year, month, day, slug|
|
266
|
-
"#{year}-#{month}-#{day} #{slug}" #=> "2011-02-16 hello"
|
267
|
-
end
|
268
|
-
|
269
|
-
# GET /username/foobar branch
|
270
|
-
r.on "username", String, method: :get do |username|
|
271
|
-
user = User.find_by_username(username)
|
272
|
-
|
273
|
-
# GET /username/foobar/posts
|
274
|
-
r.is "posts" do
|
275
|
-
# You can access user here, because the blocks are closures.
|
276
|
-
"Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
|
277
|
-
end
|
278
|
-
|
279
|
-
# GET /username/foobar/following
|
280
|
-
r.is "following" do
|
281
|
-
user.following.size.to_s #=> "1301"
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
# /search?q=barbaz
|
286
|
-
r.get "search" do
|
287
|
-
"Searched for #{r.params['q']}" #=> "Searched for barbaz"
|
288
|
-
end
|
289
|
-
|
290
|
-
r.is "login" do
|
291
|
-
# GET /login
|
292
|
-
r.get do
|
293
|
-
"Login"
|
294
|
-
end
|
295
|
-
|
296
|
-
# POST /login?user=foo&password=baz
|
297
|
-
r.post do
|
298
|
-
"#{r.params['user']}:#{r.params['password']}" #=> "foo:baz"
|
299
|
-
end
|
300
|
-
end
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
Here's a description of the matchers.
|
305
|
-
Note that "segment", as used here, means one part of the path preceded by a +/+.
|
306
|
-
So, a path such as +/foo/bar//baz+ has four segments: +/foo+, +/bar+, +/+, and +/baz+.
|
307
|
-
The +/+ here is considered the empty segment.
|
308
|
-
|
309
|
-
=== String
|
310
|
-
|
311
|
-
If a string does not contain a slash, it matches a single segment
|
312
|
-
containing the text of the string, preceded by a slash.
|
313
|
-
|
314
|
-
"" # matches "/"
|
315
|
-
"foo" # matches "/foo"
|
316
|
-
"foo" # does not match "/food"
|
317
|
-
|
318
|
-
If a string contains any slashes, it matches one additional segment for each slash:
|
319
|
-
|
320
|
-
"foo/bar" # matches "/foo/bar"
|
321
|
-
"foo/bar" # does not match "/foo/bard"
|
322
|
-
|
323
|
-
=== Regexp
|
324
|
-
|
325
|
-
Regexps match one or more segments by looking for the pattern,
|
326
|
-
preceded by a slash, and followed by a slash or the end of the path:
|
327
|
-
|
328
|
-
/foo\w+/ # matches "/foobar"
|
329
|
-
/foo\w+/ # does not match "/foo/bar"
|
330
|
-
/foo/i # matches "/foo", "/Foo/"
|
331
|
-
/foo/i # does not match "/food"
|
332
|
-
|
333
|
-
If any patterns are captured by the Regexp, they are yielded:
|
334
|
-
|
335
|
-
/foo\w+/ # matches "/foobar", yields nothing
|
336
|
-
/foo(\w+)/ # matches "/foobar", yields "bar"
|
337
|
-
|
338
|
-
=== Class
|
339
|
-
|
340
|
-
There are two classes that are supported as matchers, String
|
341
|
-
and Integer.
|
342
|
-
|
343
|
-
String :: matches any non-empty segment, yielding the segment except for
|
344
|
-
the preceding slash
|
345
|
-
Integer :: matches any segment of 0-9, returns matched values as integers
|
346
|
-
|
347
|
-
Using String and Integer is the recommended way to handle
|
348
|
-
arbitrary segments
|
349
|
-
|
350
|
-
String # matches "/foo", yields "foo"
|
351
|
-
String # matches "/1", yields "1"
|
352
|
-
String # does not match "/"
|
353
|
-
|
354
|
-
Integer # does not match "/foo"
|
355
|
-
Integer # matches "/1", yields 1
|
356
|
-
Integer # does not match "/"
|
357
|
-
|
358
|
-
=== Symbol
|
359
|
-
|
360
|
-
Symbols match any nonempty segment,
|
361
|
-
yielding the segment except for the preceding slash:
|
362
|
-
|
363
|
-
:id # matches "/foo" yields "foo"
|
364
|
-
:id # does not match "/"
|
365
|
-
|
366
|
-
Symbol matchers operate the same as the class String matcher,
|
367
|
-
and is the historical way to do arbitrary segment matching.
|
368
|
-
It is recommended to use the class String matcher in new code
|
369
|
-
as it is a bit more intuitive.
|
370
|
-
|
371
|
-
=== Proc
|
372
|
-
|
373
|
-
Procs match unless they return false or nil:
|
374
|
-
|
375
|
-
proc{true} # matches anything
|
376
|
-
proc{false} # does not match anything
|
377
|
-
|
378
|
-
Procs don't capture anything by default,
|
379
|
-
but they can do so if you add the captured text to +r.captures+.
|
380
|
-
|
381
|
-
=== Arrays
|
382
|
-
|
383
|
-
Arrays match when any of their elements match.
|
384
|
-
If multiple matchers are given to +r.on+, they all must match (an AND condition).
|
385
|
-
If an array of matchers is given, only one needs to match (an OR condition).
|
386
|
-
Evaluation stops at the first matcher that matches.
|
387
|
-
|
388
|
-
Additionally, if the matched object is a String, the string is yielded.
|
389
|
-
This makes it easy to handle multiple strings without a Regexp:
|
390
|
-
|
391
|
-
['page1', 'page2'] # matches "/page1", "/page2"
|
392
|
-
[] # does not match anything
|
393
|
-
|
394
|
-
=== Hash
|
395
|
-
|
396
|
-
Hashes allow easily calling specialized match methods on the request.
|
397
|
-
The default registered matchers included with Roda are documented below.
|
398
|
-
Some plugins add additional hash matchers, and the hash_matcher plugin
|
399
|
-
allows for easily defining your own:
|
400
|
-
|
401
|
-
class App < Roda
|
402
|
-
plugin :hash_matcher
|
403
|
-
|
404
|
-
hash_matcher(:foo) do |v|
|
405
|
-
# ...
|
406
|
-
end
|
407
|
-
|
408
|
-
route do |r|
|
409
|
-
r.on foo: 'bar' do
|
410
|
-
# ...
|
411
|
-
end
|
412
|
-
end
|
413
|
-
end
|
414
|
-
|
415
|
-
==== :all
|
416
|
-
|
417
|
-
The +:all+ matcher matches if all of the entries in the given array match, so
|
418
|
-
|
419
|
-
r.on all: [String, String] do
|
420
|
-
# ...
|
421
|
-
end
|
422
|
-
|
423
|
-
is the same as:
|
424
|
-
|
425
|
-
r.on String, String do
|
426
|
-
# ...
|
427
|
-
end
|
428
|
-
|
429
|
-
The reason it also exists as a separate hash matcher
|
430
|
-
is so you can use it inside an array matcher, so:
|
431
|
-
|
432
|
-
r.on ['foo', {all: ['foos', Integer]}] do
|
433
|
-
end
|
434
|
-
|
435
|
-
would match +/foo+ and +/foos/10+, but not +/foos+.
|
436
|
-
|
437
|
-
==== :method
|
438
|
-
|
439
|
-
The +:method+ matcher matches the method of the request.
|
440
|
-
You can provide an array to specify multiple request methods and match on any of them:
|
441
|
-
|
442
|
-
{method: :post} # matches POST
|
443
|
-
{method: ['post', 'patch']} # matches POST and PATCH
|
444
|
-
|
445
|
-
=== true
|
446
|
-
|
447
|
-
If +true+ is given directly as a matcher, it always matches.
|
448
|
-
|
449
|
-
=== false, nil
|
450
|
-
|
451
|
-
If +false+ or +nil+ is given directly as a matcher, it doesn't match anything.
|
452
|
-
|
453
|
-
=== Everything else
|
454
|
-
|
455
|
-
Everything else raises an error, unless support is specifically added for it
|
456
|
-
(some plugins add support for additional matcher types).
|
457
|
-
|
458
|
-
== Optional segments
|
459
|
-
|
460
|
-
There are multiple ways you can handle optional segments in Roda. For example,
|
461
|
-
let's say you want to accept both +/items/123+ and +/items/123/456+, with 123 being
|
462
|
-
the item's id, and 456 being some optional data.
|
463
|
-
|
464
|
-
The simplest way to handle this is by treating this as two separate routes with a
|
465
|
-
shared branch:
|
466
|
-
|
467
|
-
r.on "items", Integer do |item_id|
|
468
|
-
# Shared code for branch here
|
469
|
-
|
470
|
-
# /items/123/456
|
471
|
-
r.is Integer do |optional_data|
|
472
|
-
end
|
473
|
-
|
474
|
-
# /items/123
|
475
|
-
r.is do
|
476
|
-
end
|
477
|
-
end
|
478
|
-
|
479
|
-
This works well for many cases, but there are also cases where you really want to
|
480
|
-
treat it as one route with an optional segment. One simple way to do that is to
|
481
|
-
use a parameter instead of an optional segment (e.g. <tt>/items/123?opt=456</tt>).
|
482
|
-
|
483
|
-
r.is "items", Integer do |item_id|
|
484
|
-
optional_data = r.params['opt'].to_s
|
485
|
-
end
|
486
|
-
|
487
|
-
However, if you really do want to use a optional segment, there are a couple different
|
488
|
-
ways to use matchers to do so. One is using an array matcher where the last element
|
489
|
-
is true:
|
490
|
-
|
491
|
-
r.is "items", Integer, [String, true] do |item_id, optional_data|
|
492
|
-
end
|
493
|
-
|
494
|
-
Note that this technically yields only one argument instead of two arguments if the
|
495
|
-
optional segment isn't provided.
|
496
|
-
|
497
|
-
An alternative way to implement this is via a regexp:
|
498
|
-
|
499
|
-
r.is "items", /(\d+)(?:\/(\d+))?/ do |item_id, optional_data|
|
500
|
-
end
|
501
|
-
|
502
|
-
== Match/Route Block Return Values
|
503
|
-
|
504
|
-
If the response body has already been written to by calling +response.write+
|
505
|
-
directly, then any return value of a match block or route block is ignored.
|
506
|
-
|
507
|
-
If the response body has not already been written to, then the match block
|
508
|
-
or route block return value is inspected:
|
509
|
-
|
510
|
-
String :: used as the response body
|
511
|
-
nil, false :: ignored
|
512
|
-
everything else :: raises an error
|
513
|
-
|
514
|
-
Plugins can add support for additional match block and route block return
|
515
|
-
values. One example of this is the json plugin, which allows returning
|
516
|
-
arrays and hashes in match and route blocks and converts those directly
|
517
|
-
to JSON and uses the JSON as the response body.
|
518
|
-
|
519
|
-
== Status Codes
|
520
|
-
|
521
|
-
When it comes time to finalize a response,
|
522
|
-
if a status code has not been set manually and anything has been written to the response,
|
523
|
-
the response will use a 200 status code.
|
524
|
-
Otherwise, it will use a 404 status code.
|
525
|
-
This enables the principle of least surprise to work:
|
526
|
-
if you don't handle an action, a 404 response is assumed.
|
527
|
-
|
528
|
-
You can always set the status code manually,
|
529
|
-
via the +status+ attribute for the response.
|
530
|
-
|
531
|
-
route do |r|
|
532
|
-
r.get "hello" do
|
533
|
-
response.status = 200
|
534
|
-
end
|
535
|
-
end
|
536
|
-
|
537
|
-
When redirecting, the response will use a 302 status code by default.
|
538
|
-
You can change this by passing a second argument to +r.redirect+:
|
539
|
-
|
540
|
-
route do |r|
|
541
|
-
r.get "hello" do
|
542
|
-
r.redirect "/other", 301 # use 301 Moved Permanently
|
543
|
-
end
|
544
|
-
end
|
545
|
-
|
546
|
-
== Verb Methods
|
547
|
-
|
548
|
-
As displayed above, Roda has +r.get+ and +r.post+ methods
|
549
|
-
for matching based on the HTTP request method. If you want
|
550
|
-
to match on other HTTP request methods, use the all_verbs
|
551
|
-
plugin.
|
552
|
-
|
553
|
-
When called without any arguments, these match as long
|
554
|
-
as the request has the appropriate method, so:
|
555
|
-
|
556
|
-
r.get do end
|
557
|
-
|
558
|
-
matches any +GET+ request, and
|
559
|
-
|
560
|
-
r.post do end
|
561
|
-
|
562
|
-
matches any +POST+ request
|
563
|
-
|
564
|
-
If any arguments are given to the method, these match only
|
565
|
-
if the request method matches, all arguments match, and
|
566
|
-
the path has been fully matched by the arguments, so:
|
567
|
-
|
568
|
-
r.post "" do end
|
569
|
-
|
570
|
-
matches only +POST+ requests where the current path is +/+.
|
571
|
-
|
572
|
-
r.get "a/b" do end
|
573
|
-
|
574
|
-
matches only +GET+ requests where the current path is +/a/b+.
|
575
|
-
|
576
|
-
The reason for this difference in behavior is that
|
577
|
-
if you are not providing any arguments, you probably don't want
|
578
|
-
to also test for an exact match with the current path.
|
579
|
-
If that is something you do want, you can provide +true+ as an argument:
|
580
|
-
|
581
|
-
r.on "foo" do
|
582
|
-
r.get true do # Matches GET /foo, not GET /foo/.*
|
583
|
-
end
|
584
|
-
end
|
585
|
-
|
586
|
-
If you want to match the request method
|
587
|
-
and do only a partial match on the request path,
|
588
|
-
you need to use +r.on+ with the <tt>:method</tt> hash matcher:
|
589
|
-
|
590
|
-
r.on "foo", method: :get do # Matches GET /foo(/.*)?
|
591
|
-
end
|
592
|
-
|
593
|
-
== Root Method
|
594
|
-
|
595
|
-
As displayed above, you can also use +r.root+ as a match method.
|
596
|
-
This method matches +GET+ requests where the current path is +/+.
|
597
|
-
+r.root+ is similar to <tt>r.get ""</tt>,
|
598
|
-
except that it does not consume the +/+ from the path.
|
599
|
-
|
600
|
-
Unlike the other matching methods, +r.root+ takes no arguments.
|
601
|
-
|
602
|
-
Note that +r.root+ does not match if the path is empty;
|
603
|
-
you should use <tt>r.get true</tt> for that.
|
604
|
-
If you want to match either the empty path or +/+,
|
605
|
-
you can use <tt>r.get ["", true]</tt>, or use the slash_path_empty
|
606
|
-
plugin.
|
607
|
-
|
608
|
-
Note that +r.root+ only matches +GET+ requests.
|
609
|
-
So, to handle <tt>POST /</tt> requests, use <tt>r.post ''</tt>.
|
610
|
-
|
611
|
-
== Request and Response
|
612
|
-
|
613
|
-
While the request object is yielded to the +route+ block,
|
614
|
-
it is also available via the +request+ method.
|
615
|
-
Likewise, the response object is available via the +response+ method.
|
616
|
-
|
617
|
-
The request object is an instance of a subclass of <tt>Rack::Request</tt>,
|
618
|
-
with some additional methods.
|
619
|
-
|
620
|
-
If you want to extend the request and response objects with additional modules,
|
621
|
-
you can use the module_include plugin.
|
622
|
-
|
623
|
-
== Pollution
|
624
|
-
|
625
|
-
Roda tries very hard to avoid polluting the scope of the +route+ block.
|
626
|
-
This should make it unlikely that Roda will cause namespace issues
|
627
|
-
with your application code. Some of the things Roda does:
|
628
|
-
|
629
|
-
- The only instance variables defined by default in the scope of the +route+ block
|
630
|
-
are <tt>@_request</tt> and <tt>@_response</tt>. All instance variables in the
|
631
|
-
scope of the +route+ block used by plugins that ship with Roda are prefixed
|
632
|
-
with an underscore.
|
633
|
-
- The main methods defined, beyond the default methods for +Object+, are
|
634
|
-
+env+, +opts+, +request+, +response+, and +session+. +call+ and +_call+ are also
|
635
|
-
defined, but are deprecated. All other methods defined are prefixed with +_roda_+
|
636
|
-
- Constants inside the Roda namespace are all prefixed with +Roda+
|
637
|
-
(e.g., <tt>Roda::RodaRequest</tt>).
|
638
|
-
|
639
|
-
== Composition
|
640
|
-
|
641
|
-
You can mount any Rack app (including another Roda app), with its own middlewares,
|
642
|
-
inside a Roda app, using +r.run+:
|
643
|
-
|
644
|
-
class API < Roda
|
645
|
-
route do |r|
|
646
|
-
r.is do
|
647
|
-
# ...
|
648
|
-
end
|
649
|
-
end
|
650
|
-
end
|
651
|
-
|
652
|
-
class App < Roda
|
653
|
-
route do |r|
|
654
|
-
r.on "api" do
|
655
|
-
r.run API
|
656
|
-
end
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
|
-
run App.app
|
661
|
-
|
662
|
-
This will take any path starting with +/api+ and send it to +API+.
|
663
|
-
In this example, +API+ is a Roda app, but it could easily be
|
664
|
-
a Sinatra, Rails, or other Rack app.
|
665
|
-
|
666
|
-
When you use +r.run+, Roda calls the given Rack app (+API+ in this case);
|
667
|
-
whatever the Rack app returns will be returned
|
668
|
-
as the response for the current application.
|
669
|
-
|
670
|
-
If you have a lot of rack applications that you want to dispatch to, and
|
671
|
-
which one to dispatch to is based on the request path prefix, look into the
|
672
|
-
+multi_run+ plugin.
|
673
|
-
|
674
|
-
=== hash_branches plugin
|
675
|
-
|
676
|
-
If you are just looking to split up the main route block up by branches,
|
677
|
-
you should use the +hash_branches+ plugin,
|
678
|
-
which keeps the current scope of the +route+ block:
|
679
|
-
|
680
|
-
class App < Roda
|
681
|
-
plugin :hash_branches
|
682
|
-
|
683
|
-
hash_branch "api" do |r|
|
684
|
-
r.is do
|
685
|
-
# ...
|
686
|
-
end
|
687
|
-
end
|
688
|
-
|
689
|
-
route do |r|
|
690
|
-
r.hash_branches
|
691
|
-
end
|
692
|
-
end
|
693
|
-
|
694
|
-
run App.app
|
695
|
-
|
696
|
-
This allows you to set instance variables in the main +route+ block
|
697
|
-
and still have access to them inside the +api+ +route+ block.
|
698
|
-
|
699
|
-
== Testing
|
700
|
-
|
701
|
-
It is very easy to test Roda with {Rack::Test}[https://github.com/rack-test/rack-test]
|
702
|
-
or {Capybara}[https://github.com/teamcapybara/capybara].
|
703
|
-
Roda's own tests use {minitest/spec}[https://github.com/seattlerb/minitest].
|
704
|
-
The default Rake task will run the specs for Roda.
|
705
|
-
|
706
|
-
== Settings
|
707
|
-
|
708
|
-
Each Roda app can store settings in the +opts+ hash.
|
709
|
-
The settings are inherited by subclasses.
|
710
|
-
|
711
|
-
Roda.opts[:layout] = "guest"
|
712
|
-
|
713
|
-
class Users < Roda; end
|
714
|
-
class Admin < Roda
|
715
|
-
opts[:layout] = "admin"
|
716
|
-
end
|
717
|
-
|
718
|
-
Users.opts[:layout] # => 'guest'
|
719
|
-
Admin.opts[:layout] # => 'admin'
|
720
|
-
|
721
|
-
Feel free to store whatever you find convenient.
|
722
|
-
Note that when subclassing, Roda only does a shallow clone of the settings.
|
723
|
-
|
724
|
-
If you store nested structures and plan to mutate them in subclasses,
|
725
|
-
it is your responsibility to dup the nested structures inside +Roda.inherited+
|
726
|
-
(making sure to call +super+). This should be is done so that modifications
|
727
|
-
to the parent class made after subclassing do _not_ affect the subclass, and
|
728
|
-
vice-versa.
|
729
|
-
|
730
|
-
The plugins that ship with Roda freeze their settings and only allow modification
|
731
|
-
to their settings by reloading the plugin, and external plugins are encouraged
|
732
|
-
to follow this approach.
|
733
|
-
|
734
|
-
The following options are respected by the default library or multiple plugins:
|
735
|
-
|
736
|
-
:add_script_name :: Prepend the SCRIPT_NAME for the request to paths. This is
|
737
|
-
useful if you mount the app as a path under another app.
|
738
|
-
:check_arity :: Whether arity for blocks passed to Roda should be checked
|
739
|
-
to determine if they can be used directly to define methods
|
740
|
-
or need to be wrapped. By default, for backwards compatibility,
|
741
|
-
this is true, so Roda will check blocks and handle cases where
|
742
|
-
the arity of the block does not match the expected arity. This
|
743
|
-
can be set to +:warn+ to issue warnings whenever Roda detects an
|
744
|
-
arity mismatch. If set to +false+, Roda does not check the arity
|
745
|
-
of blocks, which can result in failures at runtime if the arity
|
746
|
-
of the block does not match what Roda expects. Note that Roda
|
747
|
-
does not check the arity for lambda blocks, as those are strict
|
748
|
-
by default.
|
749
|
-
:check_dynamic_arity :: Similar to :check_arity, but used for checking blocks
|
750
|
-
where the number of arguments Roda will call the blocks
|
751
|
-
with is not possible to determine when defining the
|
752
|
-
method. By default, Roda checks arity for such methods,
|
753
|
-
but doing so actually slows the method down even if the
|
754
|
-
number of arguments matches the expected number of arguments.
|
755
|
-
:freeze_middleware :: Whether to freeze all middleware when building the rack app.
|
756
|
-
:json_parser :: A callable for parsing JSON (+JSON.parse+ in general used by
|
757
|
-
default).
|
758
|
-
:json_serializer :: A callable for serializing JSON (+to_json+ in general used
|
759
|
-
by default).
|
760
|
-
:root :: Set the root path for the app. This defaults to the current working
|
761
|
-
directory of the process.
|
762
|
-
:sessions_convert_symbols :: This should be set to +true+ if the sessions in use
|
763
|
-
do not support roundtripping of symbols (for
|
764
|
-
example, when sessions are serialized via JSON).
|
765
|
-
|
766
|
-
There may be other options supported by individual plugins, if so it will be
|
767
|
-
mentioned in the documentation for the plugin.
|
768
|
-
|
769
|
-
== Rendering
|
770
|
-
|
771
|
-
Roda ships with a +render+ plugin that provides helpers for rendering templates.
|
772
|
-
It uses {Tilt}[https://github.com/rtomayko/tilt],
|
773
|
-
a gem that interfaces with many template engines.
|
774
|
-
The +erb+ engine is used by default.
|
775
|
-
|
776
|
-
Note that in order to use this plugin you need to have Tilt installed,
|
777
|
-
along with the templating engines you want to use.
|
778
|
-
|
779
|
-
This plugin adds the +render+ and +view+ methods, for rendering templates.
|
780
|
-
By default, +view+ will render the template inside the default layout template;
|
781
|
-
+render+ will just render the template.
|
782
|
-
|
783
|
-
class App < Roda
|
784
|
-
plugin :render
|
785
|
-
|
786
|
-
route do |r|
|
787
|
-
@var = '1'
|
788
|
-
|
789
|
-
r.get "render" do
|
790
|
-
# Renders the views/home.erb template, which will have access to
|
791
|
-
# the instance variable @var, as well as local variable content.
|
792
|
-
render("home", locals: {content: "hello, world"})
|
793
|
-
end
|
794
|
-
|
795
|
-
r.get "view" do
|
796
|
-
@var2 = '1'
|
797
|
-
|
798
|
-
# Renders the views/home.erb template, which will have access to the
|
799
|
-
# instance variables @var and @var2, and takes the output of that and
|
800
|
-
# renders it inside views/layout.erb (which should yield where the
|
801
|
-
# content should be inserted).
|
802
|
-
view("home")
|
803
|
-
end
|
804
|
-
end
|
805
|
-
end
|
806
|
-
|
807
|
-
You can override the default rendering options by passing a hash to the plugin:
|
808
|
-
|
809
|
-
class App < Roda
|
810
|
-
plugin :render,
|
811
|
-
escape: true, # Automatically escape output in erb templates using Erubi's escaping support
|
812
|
-
views: 'admin_views', # Default views directory
|
813
|
-
layout_opts: {template: 'admin_layout', engine: 'html.erb'}, # Default layout options
|
814
|
-
template_opts: {default_encoding: 'UTF-8'} # Default template options
|
815
|
-
end
|
816
|
-
|
817
|
-
== Security
|
818
|
-
|
819
|
-
Web application security is a very large topic,
|
820
|
-
but here are some things you can do with Roda
|
821
|
-
to prevent some common web application vulnerabilities.
|
822
|
-
|
823
|
-
=== Session Security
|
824
|
-
|
825
|
-
By default, Roda doesn't turn on sessions, and if you don't need sessions, you can
|
826
|
-
skip this section. If you do need sessions, Roda offers two recommended ways to
|
827
|
-
implement cookie-based sessions.
|
828
|
-
|
829
|
-
If you do not need any session support in middleware, and only need session support
|
830
|
-
in the Roda application, then use the sessions plugin:
|
831
|
-
|
832
|
-
require 'roda'
|
833
|
-
class App < Roda
|
834
|
-
plugin :sessions, secret: ENV['SESSION_SECRET']
|
835
|
-
end
|
836
|
-
|
837
|
-
The +:secret+ option should be a randomly generated string of at least 64 bytes.
|
838
|
-
|
839
|
-
If you have middleware that need access to sessions, then use the +RodaSessionMiddleware+
|
840
|
-
that ships with Roda:
|
841
|
-
|
842
|
-
require 'roda'
|
843
|
-
require 'roda/session_middleware'
|
844
|
-
class App < Roda
|
845
|
-
use RodaSessionMiddleware, secret: ENV['SESSION_SECRET']
|
846
|
-
end
|
847
|
-
|
848
|
-
If you need non-cookie based sessions (such as sessions stored in a database), you
|
849
|
-
should use an appropriate external middleware.
|
850
|
-
|
851
|
-
It is possible to use other session cookie middleware such as
|
852
|
-
<tt>Rack::Session::Cookie</tt>, but other middleware may not have the same security
|
853
|
-
features that Roda's session support does. For example, the session cookies used by
|
854
|
-
the <tt>Rack::Session::Cookie</tt> middleware provided by Rack before Rack 3 are not
|
855
|
-
encrypted, just signed to prevent tampering.
|
856
|
-
|
857
|
-
For any cookie-based sessions, make sure that the necessary secrets (+:secret+ option)
|
858
|
-
are not disclosed to an attacker. Knowledge of the
|
859
|
-
secret(s) can allow an attacker to inject arbitrary session values. In the case of
|
860
|
-
<tt>Rack::Session::Cookie</tt>, that can also lead remote code execution.
|
861
|
-
|
862
|
-
=== Cross Site Request Forgery (CSRF)
|
863
|
-
|
864
|
-
CSRF can be prevented by using the +route_csrf+ plugin that ships with Roda.
|
865
|
-
The +route_csrf+ plugin uses modern security practices to create CSRF tokens,
|
866
|
-
requires request-specific tokens by default, and offers control to the user
|
867
|
-
over where in the routing tree that CSRF tokens are checked. For example, if
|
868
|
-
you are using the +public+ plugin to serve static files and the +assets+
|
869
|
-
plugin to serve assets, you wouldn't need to check for CSRF tokens for either
|
870
|
-
of those, so you could put the CSRF check after those in the routing tree,
|
871
|
-
but before handling other requests:
|
872
|
-
|
873
|
-
route do |r|
|
874
|
-
r.public
|
875
|
-
r.assets
|
876
|
-
|
877
|
-
check_csrf! # Must call this to check for valid CSRF tokens
|
878
|
-
|
879
|
-
# ...
|
880
|
-
end
|
881
|
-
|
882
|
-
|
883
|
-
=== Cross Site Scripting (XSS)
|
884
|
-
|
885
|
-
The easiest way to prevent XSS with Roda is to use a template library
|
886
|
-
that automatically escapes output by default.
|
887
|
-
The +:escape+ option to the +render+ plugin sets the ERB template processor
|
888
|
-
to escape by default, so that in your templates:
|
889
|
-
|
890
|
-
<%= '<>' %> # outputs <>
|
891
|
-
<%== '<>' %> # outputs <>
|
892
|
-
|
893
|
-
When using the +:escape+ option, you will need to ensure that your layouts
|
894
|
-
are not escaping the output of the content template:
|
895
|
-
|
896
|
-
<%== yield %> # not <%= yield %>
|
897
|
-
|
898
|
-
This support requires {Erubi}[https://github.com/jeremyevans/erubi].
|
899
|
-
|
900
|
-
=== Unexpected Parameter Types
|
901
|
-
|
902
|
-
Rack converts submitted parameters into a hash of strings, arrays, and
|
903
|
-
nested hashes. Since the user controls the submission of parameters, you
|
904
|
-
should treat any submission of parameters with caution, and should be
|
905
|
-
explicitly checking and/or converting types before using any submitted
|
906
|
-
parameters. One way to do this is explicitly after accessing them:
|
907
|
-
|
908
|
-
# Convert foo_id parameter to an integer
|
909
|
-
request.params['foo_id'].to_i
|
910
|
-
|
911
|
-
However, it is easy to forget to convert the type, and if the user
|
912
|
-
submits +foo_id+ as a hash or array, a NoMethodError will be raised.
|
913
|
-
Worse is if you do:
|
914
|
-
|
915
|
-
some_method(request.params['bar'])
|
916
|
-
|
917
|
-
Where +some_method+ supports both a string argument and a hash
|
918
|
-
argument, and you expect the parameter will be submitted as a
|
919
|
-
string, and +some_method+'s handling of a hash argument performs
|
920
|
-
an unauthorized action.
|
921
|
-
|
922
|
-
Roda ships with a +typecast_params+ plugin that can easily handle
|
923
|
-
the typecasting of submitted parameters, and it is recommended
|
924
|
-
that all Roda applications that deal with parameters use it or
|
925
|
-
another tool to explicitly convert submitted parameters to the
|
926
|
-
expected types.
|
927
|
-
|
928
|
-
=== Content Security Policy
|
929
|
-
|
930
|
-
The Content-Security-Policy HTTP header can be used to instruct
|
931
|
-
the browser on what types of content to allow and where content
|
932
|
-
can be loaded from. Roda ships with a +content_security_policy+
|
933
|
-
plugin that allows for the easy configuration of the content
|
934
|
-
security policy. Here's an example of a fairly restrictive
|
935
|
-
content security policy configuration:
|
936
|
-
|
937
|
-
class App < Roda
|
938
|
-
plugin :content_security_policy do |csp|
|
939
|
-
csp.default_src :none # deny everything by default
|
940
|
-
csp.style_src :self
|
941
|
-
csp.script_src :self
|
942
|
-
csp.connect_src :self
|
943
|
-
csp.img_src :self
|
944
|
-
csp.font_src :self
|
945
|
-
csp.form_action :self
|
946
|
-
csp.base_uri :none
|
947
|
-
csp.frame_ancestors :none
|
948
|
-
csp.block_all_mixed_content
|
949
|
-
csp.report_uri 'CSP_REPORT_URI'
|
950
|
-
end
|
951
|
-
end
|
952
|
-
|
953
|
-
=== Other Security Related HTTP Headers
|
954
|
-
|
955
|
-
You may want to look into setting the following HTTP headers, which
|
956
|
-
can be done at the web server level, but can also be done at the
|
957
|
-
application level using using the +default_headers+ plugin:
|
958
|
-
|
959
|
-
Strict-Transport-Security :: Enforces SSL/TLS Connections to the application.
|
960
|
-
X-Content-Type-Options :: Forces some browsers to respect a declared Content-Type header.
|
961
|
-
X-Frame-Options :: Provides click-jacking protection by not allowing usage inside a frame.
|
962
|
-
Only include this if you want to support and protect old browsers that
|
963
|
-
do not support Content-Security-Policy.
|
964
|
-
|
965
|
-
Example:
|
966
|
-
|
967
|
-
class App < Roda
|
968
|
-
plugin :default_headers,
|
969
|
-
'Content-Type'=>'text/html',
|
970
|
-
'Strict-Transport-Security'=>'max-age=63072000; includeSubDomains',
|
971
|
-
'X-Content-Type-Options'=>'nosniff',
|
972
|
-
'X-Frame-Options'=>'deny'
|
973
|
-
end
|
974
|
-
|
975
|
-
=== Rendering Templates Derived From User Input
|
976
|
-
|
977
|
-
Roda's rendering plugin by default checks that rendered templates are inside the views
|
978
|
-
directory. This is because rendering templates outside the views directory is not
|
979
|
-
commonly needed, and it prevents a common attack (which is especially severe if there is any
|
980
|
-
location on the file system that users can write files to).
|
981
|
-
|
982
|
-
You can specify which directories are allowed using the +:allowed_paths+ render plugin
|
983
|
-
option. If you really want to turn path checking off, you can do so via the
|
984
|
-
<tt>check_paths: false</tt> render plugin option.
|
985
|
-
|
986
|
-
== Code Reloading
|
987
|
-
|
988
|
-
Roda does not ship with integrated support for code reloading, but there are rack-based
|
989
|
-
reloaders that will work with Roda apps.
|
990
|
-
|
991
|
-
{Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used
|
992
|
-
with Roda. It requires minimal setup and handles most cases. It overrides +require+ when
|
993
|
-
activated. If it can meet the needs of your application, it's probably the best approach.
|
994
|
-
|
995
|
-
{rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast
|
996
|
-
approach to reloading while still being fairly safe, as it only reloads files that have
|
997
|
-
been modified, and unloads constants defined in the files before reloading them. It can handle
|
998
|
-
advanced cases that Zeitwerk does not support, such as classes defined in multiple files
|
999
|
-
(common when using separate route files for different routing branches in the same application).
|
1000
|
-
However, rack-unreloader does not modify core classes and using it requires modifying your
|
1001
|
-
application code to use rack-unreloader specific APIs, which may not be simple.
|
1002
|
-
|
1003
|
-
{AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
|
1004
|
-
all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
|
1005
|
-
constants and removing them when any of the reloadable loaded files changes. It overrides
|
1006
|
-
+require+ and +require_relative+ when activated (usually in the development environment). No
|
1007
|
-
configurations other than +reloadable_paths+ are required.
|
1008
|
-
|
1009
|
-
{rerun}[https://github.com/alexch/rerun] uses a fork/exec approach for loading new
|
1010
|
-
versions of your app. It work without any changes to application
|
1011
|
-
code, but may be slower as they have to reload the entire application on every change.
|
1012
|
-
However, for small apps that load quickly, it may be a good approach.
|
1013
|
-
|
1014
|
-
There is no one reloading solution that is the best for all applications and development
|
1015
|
-
approaches. Consider your needs and the tradeoffs of each of the reloading approaches,
|
1016
|
-
and pick the one you think will work best. If you are unsure where to start,
|
1017
|
-
it may be best to start with Zeitwerk, and only consider other options if it does not
|
1018
|
-
work well for you.
|
1019
|
-
|
1020
|
-
== Plugins
|
1021
|
-
|
1022
|
-
By design, Roda has a very small core, providing only the essentials.
|
1023
|
-
All nonessential features are added via plugins.
|
1024
|
-
|
1025
|
-
Roda's plugins can override any Roda method and call +super+
|
1026
|
-
to get the default behavior, which makes Roda very extensible.
|
1027
|
-
|
1028
|
-
{Roda ships with a large number of plugins}[http://roda.jeremyevans.net/documentation.html#included-plugins],
|
1029
|
-
and {some other libraries ship with support for Roda}[http://roda.jeremyevans.net/documentation.html#external].
|
1030
|
-
|
1031
|
-
=== How to create plugins
|
1032
|
-
|
1033
|
-
Authoring your own plugins is pretty straightforward.
|
1034
|
-
Plugins are just modules, which may contain any of the following modules:
|
1035
|
-
|
1036
|
-
InstanceMethods :: module included in the Roda class
|
1037
|
-
ClassMethods :: module that extends the Roda class
|
1038
|
-
RequestMethods :: module included in the class of the request
|
1039
|
-
RequestClassMethods :: module extending the class of the request
|
1040
|
-
ResponseMethods :: module included in the class of the response
|
1041
|
-
ResponseClassMethods :: module extending the class of the response
|
1042
|
-
|
1043
|
-
If the plugin responds to +load_dependencies+, it will be called first,
|
1044
|
-
and should be used if the plugin depends on another plugin.
|
1045
|
-
|
1046
|
-
If the plugin responds to +configure+, it will be called last,
|
1047
|
-
and should be used to configure the plugin.
|
1048
|
-
|
1049
|
-
Both +load_dependencies+ and +configure+ are called
|
1050
|
-
with the additional arguments and block that was given to the plugin call.
|
1051
|
-
|
1052
|
-
So, a simple plugin to add an instance method would be:
|
1053
|
-
|
1054
|
-
module MarkdownHelper
|
1055
|
-
module InstanceMethods
|
1056
|
-
def markdown(str)
|
1057
|
-
BlueCloth.new(str).to_html
|
1058
|
-
end
|
1059
|
-
end
|
1060
|
-
end
|
1061
|
-
|
1062
|
-
Roda.plugin MarkdownHelper
|
1063
|
-
|
1064
|
-
=== Registering plugins
|
1065
|
-
|
1066
|
-
If you want to ship a Roda plugin in a gem,
|
1067
|
-
but still have Roda load it automatically via <tt>Roda.plugin :plugin_name</tt>,
|
1068
|
-
you should place it where it can be required via +roda/plugins/plugin_name+
|
1069
|
-
and then have the file register it as a plugin via
|
1070
|
-
<tt>Roda::RodaPlugins.register_plugin</tt>.
|
1071
|
-
It's recommended, but not required, that you store your plugin module
|
1072
|
-
in the <tt>Roda::RodaPlugins</tt> namespace:
|
1073
|
-
|
1074
|
-
class Roda
|
1075
|
-
module RodaPlugins
|
1076
|
-
module Markdown
|
1077
|
-
module InstanceMethods
|
1078
|
-
def markdown(str)
|
1079
|
-
BlueCloth.new(str).to_html
|
1080
|
-
end
|
1081
|
-
end
|
1082
|
-
end
|
1083
|
-
|
1084
|
-
register_plugin :markdown, Markdown
|
1085
|
-
end
|
1086
|
-
end
|
1087
|
-
|
1088
|
-
To avoid namespace pollution,
|
1089
|
-
you should avoid creating your module directly in the +Roda+ namespace.
|
1090
|
-
Additionally, any instance variables created inside +InstanceMethods+
|
1091
|
-
should be prefixed with an underscore (e.g., <tt>@_variable</tt>)
|
1092
|
-
to avoid polluting the scope. Finally, do not add any constants inside
|
1093
|
-
the InstanceMethods module, add constants to the plugin module itself
|
1094
|
-
(+Markdown+ in the above example).
|
1095
|
-
|
1096
|
-
If you are planning on shipping your plugin in an external gem, it is recommended that you follow
|
1097
|
-
{standard gem naming conventions for extensions}[http://guides.rubygems.org/name-your-gem/].
|
1098
|
-
So if your plugin module is named +FooBar+, your gem name should be <tt>roda-foo_bar</tt>.
|
1099
|
-
|
1100
|
-
== No Introspection
|
1101
|
-
|
1102
|
-
Because a routing tree does not store the routes in a data structure, but
|
1103
|
-
directly executes the routing tree block, you cannot introspect the routes
|
1104
|
-
when using a routing tree.
|
1105
|
-
|
1106
|
-
If you would like to introspect your routes when using Roda, there is an
|
1107
|
-
external plugin named {roda-route_list}[https://github.com/jeremyevans/roda-route_list],
|
1108
|
-
which allows you to add appropriate comments to your routing files, and
|
1109
|
-
has a parser that will parse those comments into routing metadata that
|
1110
|
-
you can then introspect.
|
1111
|
-
|
1112
|
-
== Inspiration
|
1113
|
-
|
1114
|
-
Roda was inspired by {Sinatra}[http://www.sinatrarb.com] and {Cuba}[http://cuba.is].
|
1115
|
-
It started out as a fork of Cuba, from which it borrows the idea of using a routing tree
|
1116
|
-
(which Cuba in turn took from {Rum}[https://github.com/chneukirchen/rum]).
|
1117
|
-
From Sinatra, it takes the ideas that route blocks should return the request bodies
|
1118
|
-
and that routes should be canonical.
|
1119
|
-
Roda's plugin system is based on the plugin system used by
|
1120
|
-
{Sequel}[http://sequel.jeremyevans.net].
|
1121
|
-
|
1122
|
-
== Ruby Support Policy
|
1123
|
-
|
1124
|
-
Roda fully supports the currently supported versions of Ruby (MRI) and JRuby. It may
|
1125
|
-
support unsupported versions of Ruby or JRuby, but such support may be dropped in any
|
1126
|
-
minor version if keeping it becomes a support issue. The minimum Ruby version
|
1127
|
-
required to run the current version of Roda is 1.9.2, and the minimum JRuby version is
|
1128
|
-
9.0.0.0.
|
1129
|
-
|
1130
|
-
== License
|
1131
|
-
|
1132
|
-
MIT
|
1133
|
-
|
1134
|
-
== Maintainer
|
1135
|
-
|
1136
|
-
Jeremy Evans <code@jeremyevans.net>
|