3.2 Integrating WEBrick::CGI and ERB with dRuby
WEBrick::CGI
enables you to write a CGI script with the same interfaces as a WEBrick
servlet. (Rack
is a similar library that’s also popular.) In this example, we’ll create a CGI script with WEBrick::CGI
and learn how it interacts with dRuby.
Here is a simple example of how to use WEBrick::CGI
:
my_cgi.rb | |
#!/usr/local/bin/ruby |
|
require 'webrick/cgi' |
|
require 'erb' |
|
|
|
class MyCGI < WEBrick::CGI |
|
def initialize(*args) |
|
super(*args) |
|
@erb = create_erb |
|
end |
|
|
|
def do_GET(req, res) |
|
build_page(req, res) |
|
rescue |
|
error_page(req, res) |
|
end |
|
|
|
def build_page(req, res) |
|
res["content-type"] = "text/html" |
|
res.body = @erb.result(binding) |
|
end |
|
|
|
def error_page(req, res) |
|
res["content-type"] = "text/plain" |
|
res.body = 'oops' |
|
end |
|
|
|
def create_erb |
|
ERB.new(<<EOS) |
|
<html> |
|
<head><title>Hello</title></head> |
|
<body>Hello, World.</body> |
|
</html> |
|
EOS |
|
end |
|
end |
|
|
|
MyCGI.new.start() |
Let’s create a class, inherit WEBrick::CGI
, and then implement the do_GET()
method. To use the class, instantiate it and then call the start
method. The req
and res
arguments passed to the method are the same as the one we use for the WEBrick
servlet.
So, how do you run CGI in your environment?
This is an example of running CGI in my environment (OS X):
$ vi my_cgi.rb |
|
$ chmod +x my_cgi.rb |
|
$ sudo cp my_cgi.rb /Library/WebServer/CGI-Executables/ |
You will see “Hello, World.” printed to the browser if you access the page from http://localhost/cgi-bin/my_cgi.rb.
The my_cgi.rb
script embedded ERB into the CGI script to make this example simple to explain. When you actually write the code, you should move the eRuby part into a different file.
Reminder CGI interface
In this section, we’ll make a simple ERB-based CGI interface of the Reminder
app we created in Section 1.2, Building the Reminder Application.
The CGI script will have the following functionalities as the command-based version we created earlier:
-
List to-do items
-
The ability to add an item
-
The ability to delete an item
First let’s implement the “List to-do items” functionality. We’ll make an ERB class to list items with <ul>
:
<ul> |
|
<li>1: 13:00 Meeting</li> |
|
<li>3: Return DVD on Saturday</li> |
|
<li>4: 15:00 Status report</li> |
|
<li>5: Request Ruby Hacking Guide from the library</li> |
|
</ul> |
We can write code to insert a row at <li>
iteratively. Assuming there are items in ary
, here is the code:
<ul> |
|
<% ary.each
do |k, v| %> |
|
<li><%= k %>: <%= v %></li> |
|
<% end %> |
|
</ul> |
To test this code, we need a test server with some sample data (test_reminder.rb
).
test_reminder.rb | |
require './reminder0' |
|
require 'drb/drb' |
|
require 'pp' |
|
|
|
reminder = Reminder.new |
|
reminder.add('Apply to RubyKaigi 2011') |
|
reminder.add('Buy a pomodoro timer') |
|
reminder.add('Display <a>') |
|
DRb.start_service('druby://localhost:12345', reminder) |
|
|
|
while true |
|
sleep 10 |
|
pp reminder.to_a |
|
end |
This time, we’ll create a Ruby script, rather than playing with irb
. This script first creates a Reminder
object, adds test data, and then starts dRuby. Run the script; it should print out the content of Reminder
every ten seconds. We’ll keep this script up and running. For the production environment, you should use DRb.thread.join
.
% ruby -I. test_reminder.rb |
|
[ |
|
[1, "Apply to RubyKaigi 2011"], |
|
[2, "Buy a pomodoro timer"], |
|
[3, "Display <a>"] |
|
] |
|
.... |
Next, let’s write a script to display the data (erb_reminder.rb
).
erb_reminder.rb | |
require 'erb' |
|
require 'drb/drb' |
|
|
|
class ReminderView |
|
extend ERB::DefMethod |
|
def_erb_method('to_html(there)', 'erb_reminder.erb') |
|
end |
|
|
|
there = DRbObject.new_with_uri('druby://localhost:12345') |
|
view = ReminderView.new |
|
puts view.to_html(there) |
The preceding code includes the erb_reminder.erb
script and also talks to the dRuby service started at terminal 1.
erb_reminder.erb | |
<ul> |
|
<% there.to_a.each do |k, v| %> |
|
<li>
<%= k %>: <%= v %>
</li> |
|
<% end %> |
|
</ul> |
Let’s run the script in a different terminal.
% ruby erb_reminder.rb |
|
<ul> |
|
|
|
<li>1: Apply to RubyKaigi 2011</li> |
|
|
|
<li>2: Buy a pomodoro timer</li> |
|
|
|
<li>3: Display <a></li> |
|
|
|
</ul> |
It should display the list using <li>
tags. But wait—it printed out < and > as they appear. It should escape tags like this:
<li>3: Display <a></li> |
Escaping
ERB::Util
provides several methods to help escape HTML tags and encode URL parameters.
Command |
Meaning |
---|---|
|
Escapes &, “, <, > |
|
An alias of |
|
Converts strings into URL-encoded strings |
|
An alias of |
u
and h
look strange, but they are used like this:
<ul> |
|
<% there.to_a.each
do |k, v| %> |
|
<li><%= k %>: <%=h v %></li> |
|
<% end %> |
|
</ul> |
When you write < %=h ... % >
, it has the same meaning as escaping HTML.
This looks like eRuby extends the Ruby syntax, but this is just a normal method invocation, as in the following code:
<%= h(...) %> |
To use these methods, you need to include ERB::Util
.
erb_reminder2.rb | |
require 'erb' |
|
require 'drb/drb' |
|
|
|
class ReminderView |
|
include ERB::Util |
|
extend ERB::DefMethod |
|
def_erb_method('to_html(there)', 'erb_reminder2.erb') |
|
end |
|
|
|
there = DRbObject.new_with_uri('druby://localhost:12345') |
|
view = ReminderView.new |
|
puts view.to_html(there) |
Let’s try it again.
% ruby erb_reminder2.rb |
|
<ul> |
|
|
|
<li>1: Apply to RubyKaigi 2011</li> |
|
|
|
<li>2: Buy a pomodoro timer</li> |
|
|
|
<li>3: Display <a></li> |
|
|
|
</ul> |
As you can see, the tags for the third item are properly escaped. This is because we used the h
method to escape embedded strings.
Automatically Escaping Special Characters
You can customize the behavior of the concatenated objects or concatenated methods by using inheritance.
Here is an example: ERB4Html
is an ERB class specialized to HTML that automatically escapes HTML.
erb4html.rb | |
Line 1 | require 'erb' |
- | |
- | class ERB |
- | class ERBString < String |
5 | def to_s; self; end |
- | |
- | def erb_concat(s) |
- | if self.class === s |
- | concat(s) |
10 | else |
- | concat(erb_quote(s)) |
- | end |
- | end |
- | |
15 | def erb_quote(s); s; end |
- | end |
- | end |
- | |
- | class ERB4Html < ERB |
20 | def self.quoted(s) |
- | HtmlString.new(s) |
- | end |
- | |
- | class HtmlString < ERB::ERBString |
25 | def erb_quote(s) |
- | ERB::Util::html_escape(s) |
- | end |
- | end |
- | |
30 | def set_eoutvar(compiler, eoutvar = '_erbout') |
- | compiler.put_cmd = "#{eoutvar}.concat" |
- | compiler.insert_cmd = "#{eoutvar}.erb_concat" |
- | compiler.pre_cmd = ["#{eoutvar} = ERB4Html.quoted('')"] |
- | compiler.post_cmd = [eoutvar] |
35 | end |
- | |
- | module Util |
- | def h(s) |
- | q(ERB::Util.h(s)) |
40 | end |
- | def u(s) |
- | q(ERB::Util.u(s)) |
- | end |
- | |
45 | def q(s) |
- | HtmlString.new(s) |
- | end |
- | end |
- | end |
This script has two main classes. ERBString
on line 4 quotes strings when concatenating strings, and
HtmlString
on line 24 does HTML escaping.
set_eoutvar
on line 30 sets various configurations when ERB converts from eRuby to Ruby.
ERB4Html
overrides set_eoutvar
to use HtmlString
for string concatenation.
Automatic HTML escaping using ERB isn’t perfect. You have to think about what to do when you want to pass HTML that’s already escaped. And, you have to do URL encoding as well.
To help with these situations, I added the u
and h
methods to ERB4Html::Util
, which act similarly to methods in ERB::Util
.
The methods in ERB4Html::Util
call methods in ERB::Util
and then mark them as “processed.” Another method, called q
, only does the marking.
Here is the example of ReminderView
, which uses ERB4Html
:
class ReminderView |
|
include ERB4Html::Util |
|
erb4html = ERB4Html.new(File.read('erb_reminder.erb')) |
|
erb4html.def_method(self, 'to_html(there)') |
|
end |
Some people say that automatic escaping avoids cross-site scripting (XSS) when you forget to manually add h
, but
I’m not convinced that automatic escaping using ERB4Html
improves your life.
What if you actually do not want to escape these tags?
What happens if you handle HTML that’s already escaped?
Basically, you should consider your options; whether you should automatically escape or not depends on your preferences.
I’ll show you one more example. The following ERB class will raise an exception if tainted strings are concatenated:
class ERBRestrict < ERB |
|
class RestrictString < ERB::ERBString |
|
def erb_concat(s) |
|
raise SecurityError if s.tainted? |
|
concat(s) |
|
end |
|
end |
|
def set_eoutvar(compiler, eoutvar = '_erbout') |
|
compiler.put_cmd = "#{eoutvar}.concat" |
|
compiler.insert_cmd = "#{eoutvar}.erb_concat" |
|
compiler.pre_cmd = [ |
|
"#{eoutvar} = ERBRestrict::RestrictString.new('')" |
|
] |
|
compiler.post_cmd = [eoutvar] |
|
end |
|
end |
This class is implemented using a subclass of ERBString
, similarly to when we implemented ERB4Html
.
In Ruby, if an object comes from an external resource, it gets marked as “tainted” (it returns true
when tainted?
is called).
ERBRestrict
raises an exception when you try to concatenate tainted strings.
This may suit you better if you want to keep unexpected things from happening.
You can customize h
to “untaint” the string once escaped.
The following is the full code for ERBRestrict
:
erbr.rb | |
require 'erb' |
|
class ERB |
|
class ERBString < String |
|
def to_s; self; end |
|
|
|
def erb_concat(s) |
|
if self.class === s |
|
concat(s) |
|
else |
|
concat(erb_quote(s)) |
|
end |
|
end |
|
|
|
def erb_quote(s); s; end |
|
end |
|
end |
|
class ERBRestrict < ERB |
|
class RestrictString < ERB::ERBString |
|
def erb_concat(s) |
|
raise SecurityError if s.tainted? |
|
concat(s) |
|
end |
|
end |
|
def set_eoutvar(compiler, eoutvar = '_erbout') |
|
compiler.put_cmd = "#{eoutvar}.concat" |
|
compiler.insert_cmd = "#{eoutvar}.erb_concat" |
|
compiler.pre_cmd = ["#{eoutvar} = ERBRestrict::RestrictString.new('')"] |
|
compiler.post_cmd = [eoutvar] |
|
end |
|
end |
We’ve now seen two ways to customize ERB.
ERB4HTML
automatically escapes HTML, and ERBRestrict
raises an exception when trying to concatenate tainted strings.