The dRuby Book

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:

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 &lt;a&gt;</li>​

Escaping

ERB::Util provides several methods to help escape HTML tags and encode URL parameters.

Command

Meaning

ERB::Util.html_escape(s)

Escapes &, “, <, >

ERB::Util.h(s)

An alias of ERB::Util.html_escape(s)

ERB::Util.url_encode(s)

Converts strings into URL-encoded strings

ERB::Util.u(s)

An alias of ERB::Util.url_encode(s)

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 &lt;a&gt;</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 ​
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.