eac-rack 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. data/COPYING +18 -0
  2. data/KNOWN-ISSUES +21 -0
  3. data/README +399 -0
  4. data/bin/rackup +4 -0
  5. data/contrib/rack_logo.svg +111 -0
  6. data/example/lobster.ru +4 -0
  7. data/example/protectedlobster.rb +14 -0
  8. data/example/protectedlobster.ru +8 -0
  9. data/lib/rack.rb +92 -0
  10. data/lib/rack/adapter/camping.rb +22 -0
  11. data/lib/rack/auth/abstract/handler.rb +37 -0
  12. data/lib/rack/auth/abstract/request.rb +37 -0
  13. data/lib/rack/auth/basic.rb +58 -0
  14. data/lib/rack/auth/digest/md5.rb +124 -0
  15. data/lib/rack/auth/digest/nonce.rb +51 -0
  16. data/lib/rack/auth/digest/params.rb +55 -0
  17. data/lib/rack/auth/digest/request.rb +40 -0
  18. data/lib/rack/builder.rb +80 -0
  19. data/lib/rack/cascade.rb +41 -0
  20. data/lib/rack/chunked.rb +49 -0
  21. data/lib/rack/commonlogger.rb +49 -0
  22. data/lib/rack/conditionalget.rb +47 -0
  23. data/lib/rack/config.rb +15 -0
  24. data/lib/rack/content_length.rb +29 -0
  25. data/lib/rack/content_type.rb +23 -0
  26. data/lib/rack/deflater.rb +96 -0
  27. data/lib/rack/directory.rb +157 -0
  28. data/lib/rack/etag.rb +23 -0
  29. data/lib/rack/file.rb +90 -0
  30. data/lib/rack/handler.rb +88 -0
  31. data/lib/rack/handler/cgi.rb +61 -0
  32. data/lib/rack/handler/evented_mongrel.rb +8 -0
  33. data/lib/rack/handler/fastcgi.rb +89 -0
  34. data/lib/rack/handler/lsws.rb +63 -0
  35. data/lib/rack/handler/mongrel.rb +90 -0
  36. data/lib/rack/handler/scgi.rb +62 -0
  37. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  38. data/lib/rack/handler/thin.rb +18 -0
  39. data/lib/rack/handler/webrick.rb +69 -0
  40. data/lib/rack/head.rb +19 -0
  41. data/lib/rack/lint.rb +575 -0
  42. data/lib/rack/lobster.rb +65 -0
  43. data/lib/rack/lock.rb +16 -0
  44. data/lib/rack/logger.rb +20 -0
  45. data/lib/rack/methodoverride.rb +27 -0
  46. data/lib/rack/mime.rb +206 -0
  47. data/lib/rack/mock.rb +189 -0
  48. data/lib/rack/nulllogger.rb +18 -0
  49. data/lib/rack/recursive.rb +57 -0
  50. data/lib/rack/reloader.rb +109 -0
  51. data/lib/rack/request.rb +271 -0
  52. data/lib/rack/response.rb +149 -0
  53. data/lib/rack/rewindable_input.rb +100 -0
  54. data/lib/rack/runtime.rb +27 -0
  55. data/lib/rack/sendfile.rb +142 -0
  56. data/lib/rack/server.rb +212 -0
  57. data/lib/rack/session/abstract/id.rb +140 -0
  58. data/lib/rack/session/cookie.rb +90 -0
  59. data/lib/rack/session/memcache.rb +119 -0
  60. data/lib/rack/session/pool.rb +100 -0
  61. data/lib/rack/showexceptions.rb +349 -0
  62. data/lib/rack/showstatus.rb +106 -0
  63. data/lib/rack/static.rb +38 -0
  64. data/lib/rack/urlmap.rb +56 -0
  65. data/lib/rack/utils.rb +614 -0
  66. data/rack.gemspec +38 -0
  67. data/test/spec_rack_auth_basic.rb +73 -0
  68. data/test/spec_rack_auth_digest.rb +226 -0
  69. data/test/spec_rack_builder.rb +84 -0
  70. data/test/spec_rack_camping.rb +51 -0
  71. data/test/spec_rack_cascade.rb +48 -0
  72. data/test/spec_rack_cgi.rb +89 -0
  73. data/test/spec_rack_chunked.rb +62 -0
  74. data/test/spec_rack_commonlogger.rb +61 -0
  75. data/test/spec_rack_conditionalget.rb +41 -0
  76. data/test/spec_rack_config.rb +24 -0
  77. data/test/spec_rack_content_length.rb +43 -0
  78. data/test/spec_rack_content_type.rb +30 -0
  79. data/test/spec_rack_deflater.rb +127 -0
  80. data/test/spec_rack_directory.rb +61 -0
  81. data/test/spec_rack_etag.rb +17 -0
  82. data/test/spec_rack_fastcgi.rb +89 -0
  83. data/test/spec_rack_file.rb +75 -0
  84. data/test/spec_rack_handler.rb +43 -0
  85. data/test/spec_rack_head.rb +30 -0
  86. data/test/spec_rack_lint.rb +528 -0
  87. data/test/spec_rack_lobster.rb +45 -0
  88. data/test/spec_rack_lock.rb +38 -0
  89. data/test/spec_rack_logger.rb +21 -0
  90. data/test/spec_rack_methodoverride.rb +60 -0
  91. data/test/spec_rack_mock.rb +243 -0
  92. data/test/spec_rack_mongrel.rb +189 -0
  93. data/test/spec_rack_nulllogger.rb +13 -0
  94. data/test/spec_rack_recursive.rb +77 -0
  95. data/test/spec_rack_request.rb +545 -0
  96. data/test/spec_rack_response.rb +221 -0
  97. data/test/spec_rack_rewindable_input.rb +118 -0
  98. data/test/spec_rack_runtime.rb +35 -0
  99. data/test/spec_rack_sendfile.rb +86 -0
  100. data/test/spec_rack_session_cookie.rb +73 -0
  101. data/test/spec_rack_session_memcache.rb +273 -0
  102. data/test/spec_rack_session_pool.rb +172 -0
  103. data/test/spec_rack_showexceptions.rb +21 -0
  104. data/test/spec_rack_showstatus.rb +72 -0
  105. data/test/spec_rack_static.rb +37 -0
  106. data/test/spec_rack_thin.rb +91 -0
  107. data/test/spec_rack_urlmap.rb +215 -0
  108. data/test/spec_rack_utils.rb +554 -0
  109. data/test/spec_rack_webrick.rb +130 -0
  110. data/test/spec_rackup.rb +154 -0
  111. metadata +311 -0
@@ -0,0 +1,349 @@
1
+ require 'ostruct'
2
+ require 'erb'
3
+ require 'rack/request'
4
+ require 'rack/utils'
5
+
6
+ module Rack
7
+ # Rack::ShowExceptions catches all exceptions raised from the app it
8
+ # wraps. It shows a useful backtrace with the sourcefile and
9
+ # clickable context, the whole Rack environment and the request
10
+ # data.
11
+ #
12
+ # Be careful when you use this on public-facing sites as it could
13
+ # reveal information helpful to attackers.
14
+
15
+ class ShowExceptions
16
+ CONTEXT = 7
17
+
18
+ def initialize(app)
19
+ @app = app
20
+ @template = ERB.new(TEMPLATE)
21
+ end
22
+
23
+ def call(env)
24
+ @app.call(env)
25
+ rescue StandardError, LoadError, SyntaxError => e
26
+ backtrace = pretty(env, e)
27
+ [500,
28
+ {"Content-Type" => "text/html",
29
+ "Content-Length" => backtrace.join.size.to_s},
30
+ backtrace]
31
+ end
32
+
33
+ def pretty(env, exception)
34
+ req = Rack::Request.new(env)
35
+ path = (req.script_name + req.path_info).squeeze("/")
36
+
37
+ frames = exception.backtrace.map { |line|
38
+ frame = OpenStruct.new
39
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
40
+ frame.filename = $1
41
+ frame.lineno = $2.to_i
42
+ frame.function = $4
43
+
44
+ begin
45
+ lineno = frame.lineno-1
46
+ lines = ::File.readlines(frame.filename)
47
+ frame.pre_context_lineno = [lineno-CONTEXT, 0].max
48
+ frame.pre_context = lines[frame.pre_context_lineno...lineno]
49
+ frame.context_line = lines[lineno].chomp
50
+ frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
51
+ frame.post_context = lines[lineno+1..frame.post_context_lineno]
52
+ rescue
53
+ end
54
+
55
+ frame
56
+ else
57
+ nil
58
+ end
59
+ }.compact
60
+
61
+ env["rack.errors"].puts "#{exception.class}: #{exception.message}"
62
+ env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l }
63
+ env["rack.errors"].flush
64
+
65
+ [@template.result(binding)]
66
+ end
67
+
68
+ def h(obj) # :nodoc:
69
+ case obj
70
+ when String
71
+ Utils.escape_html(obj)
72
+ else
73
+ Utils.escape_html(obj.inspect)
74
+ end
75
+ end
76
+
77
+ # :stopdoc:
78
+
79
+ # adapted from Django <djangoproject.com>
80
+ # Copyright (c) 2005, the Lawrence Journal-World
81
+ # Used under the modified BSD license:
82
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
83
+ TEMPLATE = <<'HTML'
84
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
85
+ <html lang="en">
86
+ <head>
87
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
88
+ <meta name="robots" content="NONE,NOARCHIVE" />
89
+ <title><%=h exception.class %> at <%=h path %></title>
90
+ <style type="text/css">
91
+ html * { padding:0; margin:0; }
92
+ body * { padding:10px 20px; }
93
+ body * * { padding:0; }
94
+ body { font:small sans-serif; }
95
+ body>div { border-bottom:1px solid #ddd; }
96
+ h1 { font-weight:normal; }
97
+ h2 { margin-bottom:.8em; }
98
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
99
+ h3 { margin:1em 0 .5em 0; }
100
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
101
+ table {
102
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
103
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
104
+ thead th {
105
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
106
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
107
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
108
+ table.vars { margin:5px 0 2px 40px; }
109
+ table.vars td, table.req td { font-family:monospace; }
110
+ table td.code { width:100%;}
111
+ table td.code div { overflow:hidden; }
112
+ table.source th { color:#666; }
113
+ table.source td {
114
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
115
+ ul.traceback { list-style-type:none; }
116
+ ul.traceback li.frame { margin-bottom:1em; }
117
+ div.context { margin: 10px 0; }
118
+ div.context ol {
119
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
120
+ div.context ol li {
121
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
122
+ div.context ol.context-line li { color:black; background-color:#ccc; }
123
+ div.context ol.context-line li span { float: right; }
124
+ div.commands { margin-left: 40px; }
125
+ div.commands a { color:black; text-decoration:none; }
126
+ #summary { background: #ffc; }
127
+ #summary h2 { font-weight: normal; color: #666; }
128
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
129
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
130
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
131
+ #explanation { background:#eee; }
132
+ #template, #template-not-exist { background:#f6f6f6; }
133
+ #template-not-exist ul { margin: 0 0 0 20px; }
134
+ #traceback { background:#eee; }
135
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
136
+ #summary table { border:none; background:transparent; }
137
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
138
+ #requestinfo h3 { margin-bottom:-1em; }
139
+ .error { background: #ffc; }
140
+ .specific { color:#cc3300; font-weight:bold; }
141
+ </style>
142
+ <script type="text/javascript">
143
+ //<!--
144
+ function getElementsByClassName(oElm, strTagName, strClassName){
145
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
146
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
147
+ var arrElements = (strTagName == "*" && document.all)? document.all :
148
+ oElm.getElementsByTagName(strTagName);
149
+ var arrReturnElements = new Array();
150
+ strClassName = strClassName.replace(/\-/g, "\\-");
151
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
152
+ var oElement;
153
+ for(var i=0; i<arrElements.length; i++){
154
+ oElement = arrElements[i];
155
+ if(oRegExp.test(oElement.className)){
156
+ arrReturnElements.push(oElement);
157
+ }
158
+ }
159
+ return (arrReturnElements)
160
+ }
161
+ function hideAll(elems) {
162
+ for (var e = 0; e < elems.length; e++) {
163
+ elems[e].style.display = 'none';
164
+ }
165
+ }
166
+ window.onload = function() {
167
+ hideAll(getElementsByClassName(document, 'table', 'vars'));
168
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
169
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
170
+ }
171
+ function toggle() {
172
+ for (var i = 0; i < arguments.length; i++) {
173
+ var e = document.getElementById(arguments[i]);
174
+ if (e) {
175
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
176
+ }
177
+ }
178
+ return false;
179
+ }
180
+ function varToggle(link, id) {
181
+ toggle('v' + id);
182
+ var s = link.getElementsByTagName('span')[0];
183
+ var uarr = String.fromCharCode(0x25b6);
184
+ var darr = String.fromCharCode(0x25bc);
185
+ s.innerHTML = s.innerHTML == uarr ? darr : uarr;
186
+ return false;
187
+ }
188
+ //-->
189
+ </script>
190
+ </head>
191
+ <body>
192
+
193
+ <div id="summary">
194
+ <h1><%=h exception.class %> at <%=h path %></h1>
195
+ <h2><%=h exception.message %></h2>
196
+ <table><tr>
197
+ <th>Ruby</th>
198
+ <td><code><%=h frames.first.filename %></code>: in <code><%=h frames.first.function %></code>, line <%=h frames.first.lineno %></td>
199
+ </tr><tr>
200
+ <th>Web</th>
201
+ <td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
202
+ </tr></table>
203
+
204
+ <h3>Jump to:</h3>
205
+ <ul id="quicklinks">
206
+ <li><a href="#get-info">GET</a></li>
207
+ <li><a href="#post-info">POST</a></li>
208
+ <li><a href="#cookie-info">Cookies</a></li>
209
+ <li><a href="#env-info">ENV</a></li>
210
+ </ul>
211
+ </div>
212
+
213
+ <div id="traceback">
214
+ <h2>Traceback <span>(innermost first)</span></h2>
215
+ <ul class="traceback">
216
+ <% frames.each { |frame| %>
217
+ <li class="frame">
218
+ <code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
219
+
220
+ <% if frame.context_line %>
221
+ <div class="context" id="c<%=h frame.object_id %>">
222
+ <% if frame.pre_context %>
223
+ <ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
224
+ <% frame.pre_context.each { |line| %>
225
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
226
+ <% } %>
227
+ </ol>
228
+ <% end %>
229
+
230
+ <ol start="<%=h frame.lineno %>" class="context-line">
231
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
232
+
233
+ <% if frame.post_context %>
234
+ <ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
235
+ <% frame.post_context.each { |line| %>
236
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
237
+ <% } %>
238
+ </ol>
239
+ <% end %>
240
+ </div>
241
+ <% end %>
242
+ </li>
243
+ <% } %>
244
+ </ul>
245
+ </div>
246
+
247
+ <div id="requestinfo">
248
+ <h2>Request information</h2>
249
+
250
+ <h3 id="get-info">GET</h3>
251
+ <% unless req.GET.empty? %>
252
+ <table class="req">
253
+ <thead>
254
+ <tr>
255
+ <th>Variable</th>
256
+ <th>Value</th>
257
+ </tr>
258
+ </thead>
259
+ <tbody>
260
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
261
+ <tr>
262
+ <td><%=h key %></td>
263
+ <td class="code"><div><%=h val.inspect %></div></td>
264
+ </tr>
265
+ <% } %>
266
+ </tbody>
267
+ </table>
268
+ <% else %>
269
+ <p>No GET data.</p>
270
+ <% end %>
271
+
272
+ <h3 id="post-info">POST</h3>
273
+ <% unless req.POST.empty? %>
274
+ <table class="req">
275
+ <thead>
276
+ <tr>
277
+ <th>Variable</th>
278
+ <th>Value</th>
279
+ </tr>
280
+ </thead>
281
+ <tbody>
282
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
283
+ <tr>
284
+ <td><%=h key %></td>
285
+ <td class="code"><div><%=h val.inspect %></div></td>
286
+ </tr>
287
+ <% } %>
288
+ </tbody>
289
+ </table>
290
+ <% else %>
291
+ <p>No POST data.</p>
292
+ <% end %>
293
+
294
+
295
+ <h3 id="cookie-info">COOKIES</h3>
296
+ <% unless req.cookies.empty? %>
297
+ <table class="req">
298
+ <thead>
299
+ <tr>
300
+ <th>Variable</th>
301
+ <th>Value</th>
302
+ </tr>
303
+ </thead>
304
+ <tbody>
305
+ <% req.cookies.each { |key, val| %>
306
+ <tr>
307
+ <td><%=h key %></td>
308
+ <td class="code"><div><%=h val.inspect %></div></td>
309
+ </tr>
310
+ <% } %>
311
+ </tbody>
312
+ </table>
313
+ <% else %>
314
+ <p>No cookie data.</p>
315
+ <% end %>
316
+
317
+ <h3 id="env-info">Rack ENV</h3>
318
+ <table class="req">
319
+ <thead>
320
+ <tr>
321
+ <th>Variable</th>
322
+ <th>Value</th>
323
+ </tr>
324
+ </thead>
325
+ <tbody>
326
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
327
+ <tr>
328
+ <td><%=h key %></td>
329
+ <td class="code"><div><%=h val %></div></td>
330
+ </tr>
331
+ <% } %>
332
+ </tbody>
333
+ </table>
334
+
335
+ </div>
336
+
337
+ <div id="explanation">
338
+ <p>
339
+ You're seeing this error because you use <code>Rack::ShowExceptions</code>.
340
+ </p>
341
+ </div>
342
+
343
+ </body>
344
+ </html>
345
+ HTML
346
+
347
+ # :startdoc:
348
+ end
349
+ end
@@ -0,0 +1,106 @@
1
+ require 'erb'
2
+ require 'rack/request'
3
+ require 'rack/utils'
4
+
5
+ module Rack
6
+ # Rack::ShowStatus catches all empty responses the app it wraps and
7
+ # replaces them with a site explaining the error.
8
+ #
9
+ # Additional details can be put into <tt>rack.showstatus.detail</tt>
10
+ # and will be shown as HTML. If such details exist, the error page
11
+ # is always rendered, even if the reply was not empty.
12
+
13
+ class ShowStatus
14
+ def initialize(app)
15
+ @app = app
16
+ @template = ERB.new(TEMPLATE)
17
+ end
18
+
19
+ def call(env)
20
+ status, headers, body = @app.call(env)
21
+ headers = Utils::HeaderHash.new(headers)
22
+ empty = headers['Content-Length'].to_i <= 0
23
+
24
+ # client or server error, or explicit message
25
+ if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
26
+ req = Rack::Request.new(env)
27
+ message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
28
+ detail = env["rack.showstatus.detail"] || message
29
+ body = @template.result(binding)
30
+ size = Rack::Utils.bytesize(body)
31
+ [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]]
32
+ else
33
+ [status, headers, body]
34
+ end
35
+ end
36
+
37
+ def h(obj) # :nodoc:
38
+ case obj
39
+ when String
40
+ Utils.escape_html(obj)
41
+ else
42
+ Utils.escape_html(obj.inspect)
43
+ end
44
+ end
45
+
46
+ # :stopdoc:
47
+
48
+ # adapted from Django <djangoproject.com>
49
+ # Copyright (c) 2005, the Lawrence Journal-World
50
+ # Used under the modified BSD license:
51
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
52
+ TEMPLATE = <<'HTML'
53
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
54
+ <html lang="en">
55
+ <head>
56
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
57
+ <title><%=h message %> at <%=h req.script_name + req.path_info %></title>
58
+ <meta name="robots" content="NONE,NOARCHIVE" />
59
+ <style type="text/css">
60
+ html * { padding:0; margin:0; }
61
+ body * { padding:10px 20px; }
62
+ body * * { padding:0; }
63
+ body { font:small sans-serif; background:#eee; }
64
+ body>div { border-bottom:1px solid #ddd; }
65
+ h1 { font-weight:normal; margin-bottom:.4em; }
66
+ h1 span { font-size:60%; color:#666; font-weight:normal; }
67
+ table { border:none; border-collapse: collapse; width:100%; }
68
+ td, th { vertical-align:top; padding:2px 3px; }
69
+ th { width:12em; text-align:right; color:#666; padding-right:.5em; }
70
+ #info { background:#f6f6f6; }
71
+ #info ol { margin: 0.5em 4em; }
72
+ #info ol li { font-family: monospace; }
73
+ #summary { background: #ffc; }
74
+ #explanation { background:#eee; border-bottom: 0px none; }
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <div id="summary">
79
+ <h1><%=h message %> <span>(<%= status.to_i %>)</span></h1>
80
+ <table class="meta">
81
+ <tr>
82
+ <th>Request Method:</th>
83
+ <td><%=h req.request_method %></td>
84
+ </tr>
85
+ <tr>
86
+ <th>Request URL:</th>
87
+ <td><%=h req.url %></td>
88
+ </tr>
89
+ </table>
90
+ </div>
91
+ <div id="info">
92
+ <p><%= detail %></p>
93
+ </div>
94
+
95
+ <div id="explanation">
96
+ <p>
97
+ You're seeing this error because you use <code>Rack::ShowStatus</code>.
98
+ </p>
99
+ </div>
100
+ </body>
101
+ </html>
102
+ HTML
103
+
104
+ # :startdoc:
105
+ end
106
+ end