rack-cache 0.3.0 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +43 -0
- data/README +18 -9
- data/Rakefile +1 -14
- data/TODO +13 -14
- data/doc/configuration.markdown +7 -153
- data/doc/faq.markdown +8 -0
- data/doc/index.markdown +7 -9
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +5 -11
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +190 -52
- data/lib/rack/cache/entitystore.rb +10 -4
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +52 -16
- data/lib/rack/cache/options.rb +60 -39
- data/lib/rack/cache/request.rb +11 -15
- data/lib/rack/cache/response.rb +221 -30
- data/lib/rack/cache/storage.rb +1 -2
- data/rack-cache.gemspec +9 -15
- data/test/cache_test.rb +9 -6
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +251 -169
- data/test/entitystore_test.rb +12 -11
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +57 -14
- data/test/options_test.rb +11 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +164 -23
- data/test/spec_setup.rb +7 -0
- metadata +12 -20
- data/doc/events.dot +0 -27
- data/lib/rack/cache/config.rb +0 -65
- data/lib/rack/cache/config/busters.rb +0 -16
- data/lib/rack/cache/config/default.rb +0 -133
- data/lib/rack/cache/config/no-cache.rb +0 -13
- data/lib/rack/cache/core.rb +0 -299
- data/lib/rack/cache/headers.rb +0 -325
- data/lib/rack/utils/environment_headers.rb +0 -78
- data/test/config_test.rb +0 -66
- data/test/core_test.rb +0 -84
- data/test/environment_headers_test.rb +0 -69
- data/test/headers_test.rb +0 -298
- data/test/logging_test.rb +0 -45
data/CHANGES
CHANGED
@@ -1,3 +1,46 @@
|
|
1
|
+
## 0.4.0 / March 2009
|
2
|
+
|
3
|
+
* Ruby 1.9.1 / Rack 1.0 compatible.
|
4
|
+
|
5
|
+
* Invalidate cache entries that match the request URL on non-GET/HEAD
|
6
|
+
requests. i.e., POST, PUT, DELETE cause matching cache entries to
|
7
|
+
be invalidated. The cache entry is validated with the backend using
|
8
|
+
a conditional GET the next time it's requested.
|
9
|
+
|
10
|
+
* Implement "Cache-Control: max-age=N" request directive by forcing
|
11
|
+
validation when the max-age provided exceeds the age of the cache
|
12
|
+
entry. This can be disabled by setting the "allow_revalidate" option to
|
13
|
+
false.
|
14
|
+
|
15
|
+
* Properly implement "Cache-Control: no-cache" request directive by
|
16
|
+
performing a full reload. RFC 2616 states that when "no-cache" is
|
17
|
+
present in the request, the cache MUST NOT serve a stored response even
|
18
|
+
after successful validation. This is slightly different from the
|
19
|
+
"no-cache" directive in responses, which indicates that the cache must
|
20
|
+
first validate its entry with the origin. Previously, we implemented
|
21
|
+
"no-cache" on requests by passing so no new cache entry would be stored
|
22
|
+
based on the response. Now we treat it as a forced miss and enter the
|
23
|
+
response into the cache if it's cacheable. This can be disabled by
|
24
|
+
setting the "allow_reload" option to false.
|
25
|
+
|
26
|
+
* Assume identical semantics for the "Pragma: no-cache" request header
|
27
|
+
as the "Cache-Control: no-cache" directive described above.
|
28
|
+
|
29
|
+
* Less crazy logging. When the verbose option is set, a single log entry
|
30
|
+
is written with a comma separated list of trace events. For example, if
|
31
|
+
the cache was stale but validated, the following log entry would be
|
32
|
+
written: "cache: stale, valid, store". When the verbose option is false,
|
33
|
+
no logging occurs.
|
34
|
+
|
35
|
+
* Added "X-Rack-Cache" response header with the same comma separated trace
|
36
|
+
value as described above. This gives some visibility into how the cache
|
37
|
+
processed the request.
|
38
|
+
|
39
|
+
* Add support for canonicalized cache keys, as well as custom cache key
|
40
|
+
generators, which are specified in the options as :cache_key as either
|
41
|
+
any object that has a call() or as a block. Cache key generators get
|
42
|
+
passed a request object and return a cache key string.
|
43
|
+
|
1
44
|
## 0.3.0 / December 2008
|
2
45
|
|
3
46
|
* Add support for public and private cache control directives. Responses
|
data/README
CHANGED
@@ -12,7 +12,6 @@ validation (Last-Modified, ETag) information:
|
|
12
12
|
* Cache-Control: public, private, max-age, s-maxage, must-revalidate,
|
13
13
|
and proxy-revalidate.
|
14
14
|
* Portable: 100% Ruby / works with any Rack-enabled framework
|
15
|
-
* Configuration language for advanced caching policies
|
16
15
|
* Disk, memcached, and heap memory storage backends
|
17
16
|
|
18
17
|
For more information about Rack::Cache features and usage, see:
|
@@ -25,14 +24,6 @@ caching solution for small to medium sized deployments. More sophisticated /
|
|
25
24
|
high-performance caching systems (e.g., Varnish, Squid, httpd/mod-cache) may be
|
26
25
|
more appropriate for large deployments with significant throughput requirements.
|
27
26
|
|
28
|
-
Status
|
29
|
-
------
|
30
|
-
|
31
|
-
Rack::Cache is a young and experimental project that is likely to change
|
32
|
-
substantially and may not be wholly functional, consistent, fast, or correct.
|
33
|
-
The current focus is on reaching basic compliance with RFC 2616 and providing
|
34
|
-
good documentation.
|
35
|
-
|
36
27
|
Installation
|
37
28
|
------------
|
38
29
|
|
@@ -66,6 +57,24 @@ Assuming you've designed your backend application to take advantage of HTTP's
|
|
66
57
|
caching features, no further code or configuration is required for basic
|
67
58
|
caching.
|
68
59
|
|
60
|
+
Using with Rails
|
61
|
+
----------------
|
62
|
+
|
63
|
+
Add this to your `config/environment.rb`:
|
64
|
+
|
65
|
+
config.middleware.use Rack::Cache,
|
66
|
+
:verbose => true,
|
67
|
+
:metastore => 'file:/var/cache/rack/meta',
|
68
|
+
:entitystore => 'file:/var/cache/rack/body'
|
69
|
+
|
70
|
+
You should now see `Rack::Cache` listed in the middleware pipeline:
|
71
|
+
|
72
|
+
rake middleware
|
73
|
+
|
74
|
+
See the following for more information:
|
75
|
+
|
76
|
+
http://snippets.aktagon.com/snippets/302
|
77
|
+
|
69
78
|
Links
|
70
79
|
-----
|
71
80
|
|
data/Rakefile
CHANGED
@@ -31,7 +31,7 @@ end
|
|
31
31
|
|
32
32
|
# DOC =======================================================================
|
33
33
|
desc 'Build all documentation'
|
34
|
-
task :doc => %w[doc:api doc:
|
34
|
+
task :doc => %w[doc:api doc:markdown]
|
35
35
|
|
36
36
|
# requires the hanna gem:
|
37
37
|
# gem install mislav-hanna --source=http://gems.github.com
|
@@ -55,19 +55,6 @@ file 'doc/api/index.html' => FileList['lib/**/*.rb'] do |f|
|
|
55
55
|
end
|
56
56
|
CLEAN.include 'doc/api'
|
57
57
|
|
58
|
-
desc 'Build graphviz graphs'
|
59
|
-
task 'doc:graphs'
|
60
|
-
%w[pdf png svg].each do |filetype|
|
61
|
-
FileList["doc/*.dot"].each do |source|
|
62
|
-
dest = source.sub(/dot$/, filetype)
|
63
|
-
file dest => source do |f|
|
64
|
-
sh "dot -T#{filetype} #{source} -o #{f.name}"
|
65
|
-
end
|
66
|
-
task 'doc:graphs' => dest
|
67
|
-
CLEAN.include dest
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
58
|
desc 'Build markdown documentation files'
|
72
59
|
task 'doc:markdown'
|
73
60
|
FileList['doc/*.markdown'].each do |source|
|
data/TODO
CHANGED
@@ -1,21 +1,20 @@
|
|
1
|
-
## 0.4
|
2
|
-
|
3
|
-
- liberal, conservative, sane caching configs
|
4
|
-
- Sample apps: Rack, Rails, Sinatra, Merb, etc.
|
5
|
-
- busters.rb and no-cache.rb doc and tests
|
6
|
-
- Canonicalized URL for cache key:
|
7
|
-
- sorts params by key, then value
|
8
|
-
- urlencodes /[^ A-Za-z0-9_.-]/ host, path, and param key/value
|
9
|
-
- Custom cache keys
|
10
|
-
- Cache invalidation on PUT, POST, DELETE.
|
11
|
-
- Invalidate at the request URI; or, anything that's "near" the request URI.
|
12
|
-
- Invalidate at the URI of the Location or Content-Location response header.
|
13
|
-
|
14
1
|
## Backlog
|
15
2
|
|
3
|
+
- Move breakers.rb configuration file into rack-contrib as a middleware
|
4
|
+
component.
|
5
|
+
- Sample apps: Rack, Rails, Sinatra, Merb, etc.
|
6
|
+
- Use Bacon instead of test/spec
|
7
|
+
- Work with both memcache and memcached gems (memcached hasn't built on MacOS
|
8
|
+
for some time now).
|
9
|
+
- Fast path pass processing. We do a lot more than necessary just to determine
|
10
|
+
that the response should be passed through untouched.
|
11
|
+
- Don't purge/remove cache entries when invalidating. The entries should be
|
12
|
+
marked as stale and be forced revalidated on the next request instead of
|
13
|
+
being removed entirely.
|
16
14
|
- Add missing Expires header if we have a max-age.
|
17
|
-
- Purge/invalidate specific cache entries
|
18
15
|
- Purge/invalidate everything
|
16
|
+
- Invalidate at the URI of the Location or Content-Location response header
|
17
|
+
on POST, PUT, or DELETE that results in a redirect.
|
19
18
|
- Maximum size of cached entity
|
20
19
|
- Last-Modified factor: requests that have a Last-Modified header but no Expires
|
21
20
|
header have a TTL assigned based on the last modified age of the response:
|
data/doc/configuration.markdown
CHANGED
@@ -1,51 +1,17 @@
|
|
1
|
-
Configuration
|
2
|
-
|
1
|
+
Configuration
|
2
|
+
=============
|
3
3
|
|
4
4
|
__Rack::Cache__ includes a configuration system that can be used to specify
|
5
5
|
fairly sophisticated cache policy on a global or per-request basis.
|
6
6
|
|
7
|
-
- [Synopsis](#synopsis)
|
8
|
-
- [Setting Cache Options](#setopt)
|
9
|
-
- [Cache Option Reference](#options)
|
10
|
-
- [Configuration Machinery - Events and Transitions](#machinery)
|
11
|
-
- [Importing Configuration](#import)
|
12
|
-
- [Default Configuration Machinery](#default)
|
13
|
-
- [Notes](#notes)
|
14
|
-
|
15
|
-
<a id='synopsis'></a>
|
16
|
-
|
17
|
-
Synopsis
|
18
|
-
--------
|
19
|
-
|
20
|
-
use Rack::Cache do
|
21
|
-
# set cache related options
|
22
|
-
set :verbose, true
|
23
|
-
set :metastore, 'memcached://localhost:11211'
|
24
|
-
set :entitystore, 'file:/var/cache/rack/body'
|
25
|
-
|
26
|
-
# override events / transitions
|
27
|
-
on :receive do
|
28
|
-
pass! if request.url =~ %r|/dontcache/|
|
29
|
-
error! 402 if request.referrer =~ /digg.com/
|
30
|
-
end
|
31
|
-
|
32
|
-
on :miss do
|
33
|
-
trace 'missed: %s', request.url
|
34
|
-
end
|
35
|
-
|
36
|
-
# bring in other configuration machinery
|
37
|
-
import 'rack/cache/config/breakers'
|
38
|
-
import 'mycacheconfig'
|
39
|
-
end
|
40
|
-
|
41
7
|
<a id='setopt'></a>
|
42
8
|
|
43
9
|
Setting Cache Options
|
44
10
|
---------------------
|
45
11
|
|
46
|
-
Cache options can be set when the __Rack::Cache__ object is created
|
47
|
-
|
48
|
-
|
12
|
+
Cache options can be set when the __Rack::Cache__ object is created,
|
13
|
+
or by setting a `rack-cache.<option>` variable in __Rack__'s
|
14
|
+
__Environment__.
|
49
15
|
|
50
16
|
When the __Rack::Cache__ object is instantiated:
|
51
17
|
|
@@ -54,14 +20,6 @@ When the __Rack::Cache__ object is instantiated:
|
|
54
20
|
:metastore => 'memcached://localhost:11211/',
|
55
21
|
:entitystore => 'file:/var/cache/rack'
|
56
22
|
|
57
|
-
Using the `set` method within __Rack::Cache__'s configuration context:
|
58
|
-
|
59
|
-
use Rack::Cache do
|
60
|
-
set :verbose, true
|
61
|
-
set :metastore, 'memcached://localhost:11211/'
|
62
|
-
set :entitystore, 'file:/var/cache/rack'
|
63
|
-
end
|
64
|
-
|
65
23
|
Using __Rack__'s __Environment__:
|
66
24
|
|
67
25
|
env.merge!(
|
@@ -123,110 +81,6 @@ If any of these headers are present in the request, the response is considered
|
|
123
81
|
private and will not be cached _unless_ the response is explicitly marked public
|
124
82
|
(e.g., `Cache-Control: public`).
|
125
83
|
|
126
|
-
|
127
|
-
|
128
|
-
Configuration Machinery - Events and Transitions
|
129
|
-
------------------------------------------------
|
130
|
-
|
131
|
-
The configuration machinery is built around a series of interceptable events and
|
132
|
-
transitions controlled by a simple configuration language. The following diagram
|
133
|
-
shows each state (interceptable event) along with their possible transitions:
|
134
|
-
|
135
|
-
<p class='center'>
|
136
|
-
<img src='events.png' alt='Events and Transitions Diagram' />
|
137
|
-
</p>
|
138
|
-
|
139
|
-
Custom logic can be layered onto the `receive`, `hit`, `miss`, `fetch`, `store`,
|
140
|
-
`deliver`, and `pass` events by passing a block to the `on` method:
|
141
|
-
|
142
|
-
on :fetch do
|
143
|
-
trace 'fetched %p from backend application', request.url
|
144
|
-
end
|
145
|
-
|
146
|
-
Here, the `trace` method writes a message to the `rack.errors` stream when a
|
147
|
-
response is fetched from the backend application. The `request` object is a
|
148
|
-
[__Rack::Cache::Request__](./api/classes/Rack/Cache/Request) that can be
|
149
|
-
inspected (and modified) to determine what action should be taken next.
|
150
|
-
|
151
|
-
Event blocks are capable of performing more interesting operations:
|
152
|
-
|
153
|
-
* Transition to a different event or override default caching logic.
|
154
|
-
* Modify the request, response, cache entry, or Rack environment options.
|
155
|
-
* Set the `metastore` or `entitystore` options to select a different storage
|
156
|
-
mechanism / location dynamically.
|
157
|
-
* Collect statistics or log request/response/cache information.
|
158
|
-
|
159
|
-
When an event is triggered, the blocks associated with the event are executed in
|
160
|
-
reverse/FILO order (i.e., the block declared last runs first) until a
|
161
|
-
_transitioning statement_ is encountered. Transitioning statements are suffixed
|
162
|
-
with a bang character (e.g, `pass!`, `store!`, `error!`) and cause the current
|
163
|
-
event to halt and the machine to transition to the subsequent event; control is
|
164
|
-
not returned to the original event. The [default configuration](#default)
|
165
|
-
includes documentation on available transitions for each event.
|
166
|
-
|
167
|
-
The `next` statement can be used to exit an event block without transitioning
|
168
|
-
to another event. Subsequent event blocks are executed until a transitioning
|
169
|
-
statement is encountered:
|
170
|
-
|
171
|
-
on :fetch do
|
172
|
-
next if response.freshness_information?
|
173
|
-
|
174
|
-
if request.url =~ /\/feed$/
|
175
|
-
trace 'feed will expire in fifteen minutes'
|
176
|
-
response.ttl = 15 * 60
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
<a id='import'></a>
|
181
|
-
|
182
|
-
Importing Configuration
|
183
|
-
-----------------------
|
184
|
-
|
185
|
-
Since caching logic can be layered, it's possible to separate various bits of
|
186
|
-
cache policy into files for organization and reuse.
|
187
|
-
|
188
|
-
use Rack::Cache do
|
189
|
-
import 'rack/cache/config/busters'
|
190
|
-
import 'mycacheconfig'
|
191
|
-
|
192
|
-
# more stuff here
|
193
|
-
end
|
194
|
-
|
195
|
-
The `busters` and `mycacheconfig` configuration files are normal Ruby source
|
196
|
-
files (i.e., they have a `.rb` extension) situated on the `$LOAD_PATH` - the
|
197
|
-
`import` statement works like Ruby's `require` statement but the contents of the
|
198
|
-
files are evaluated in the context of the configuration machinery, as if
|
199
|
-
specified directly in the configuration block.
|
200
|
-
|
201
|
-
The `rack/cache/config/busters.rb` file makes a good example. It hooks into the
|
202
|
-
`fetch` event and adds an impractically long expiration lifetime to any response
|
203
|
-
that includes a cache busting query string:
|
204
|
-
|
205
|
-
<%= File.read('lib/rack/cache/config/busters.rb').gsub(/^/, ' ') %>
|
206
|
-
|
207
|
-
|
208
|
-
<a id='default'></a>
|
209
|
-
|
210
|
-
Default Configuration Machinery
|
211
|
-
-------------------------------
|
212
|
-
|
213
|
-
The `rack/cache/config/default.rb` file is imported when the __Rack::Cache__
|
214
|
-
object is instantiated and before any custom configuration code is executed.
|
215
|
-
It's useful to understand this configuration because it drives the default
|
216
|
-
transitioning logic.
|
217
|
-
|
218
|
-
<%= File.read('lib/rack/cache/config/default.rb').gsub(/^/, ' ') %>
|
219
|
-
|
220
|
-
<a id='notes'></a>
|
221
|
-
|
222
|
-
Notes
|
223
|
-
-----
|
224
|
-
|
225
|
-
The configuration language was inspired by [Varnish][var]'s
|
226
|
-
[VCL configuration language][vcl].
|
227
|
-
|
228
|
-
[var]: http://varnish.projects.linpro.no/
|
229
|
-
"Varnish HTTP accelerator"
|
84
|
+
### `cache_key`
|
230
85
|
|
231
|
-
|
232
|
-
"VCL(7) -- Varnish Configuration Language Manual Page"
|
86
|
+
TODO: Document custom cache keys
|
data/doc/faq.markdown
CHANGED
@@ -10,6 +10,14 @@ General
|
|
10
10
|
-------
|
11
11
|
|
12
12
|
|
13
|
+
<a class='hash' id='rails' href='#rails'>#</a>
|
14
|
+
|
15
|
+
### Q: Can I use Rack::Cache with Rails?
|
16
|
+
|
17
|
+
Rack::Cache can be used with Rails 2.3 or above. Documentation and a
|
18
|
+
sample application is forthcoming; in the mean time, see
|
19
|
+
[this example of using Rack::Cache with Rails 2.3](http://snippets.aktagon.com/snippets/302-How-to-setup-and-use-Rack-Cache-with-Rails-2-3-0-RC-1).
|
20
|
+
|
13
21
|
<a class='hash' id='why-not-squid' href='#why-not-squid'>#</a>
|
14
22
|
|
15
23
|
### Q: Why Rack::Cache? Why not Squid, Varnish, Perlbol, etc.?
|
data/doc/index.markdown
CHANGED
@@ -7,16 +7,15 @@ for [Rack][]-based applications that produce freshness (`Expires`,
|
|
7
7
|
* Validation
|
8
8
|
* Vary support
|
9
9
|
* Portable: 100% Ruby / works with any [Rack][]-enabled framework.
|
10
|
-
* [Configuration language][config] for advanced caching policies.
|
11
10
|
* Disk, memcached, and heap memory [storage backends][storage].
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
News
|
13
|
+
----
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
* [How to use Rack::Cache with Rails 2.3](http://snippets.aktagon.com/snippets/302-How-to-setup-and-use-Rack-Cache-with-Rails-2-3-0-RC-1) - it's really easy.
|
16
|
+
* [RailsLab's Advanced HTTP Caching Screencast](http://railslab.newrelic.com/2009/02/26/episode-11-advanced-http-caching)
|
17
|
+
is a really great review of HTTP caching concepts and shows how to
|
18
|
+
use Rack::Cache with Rails.
|
20
19
|
|
21
20
|
Installation
|
22
21
|
------------
|
@@ -52,8 +51,7 @@ caching.
|
|
52
51
|
More
|
53
52
|
----
|
54
53
|
|
55
|
-
* [Configuration
|
56
|
-
policy using the simple event-based configuration system.
|
54
|
+
* [Configuration Options][config] - how to set cache options.
|
57
55
|
|
58
56
|
* [Cache Storage Documentation][storage] - detailed information on the various
|
59
57
|
storage implementations available in __Rack::Cache__ and how to choose the one
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'rack/cache'
|
3
|
+
|
4
|
+
use Rack::Cache do
|
5
|
+
set :verbose, true
|
6
|
+
set :metastore, 'heap:/'
|
7
|
+
set :entitystore, 'heap:/'
|
8
|
+
|
9
|
+
on :receive do
|
10
|
+
pass! if request.url =~ /favicon/
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
before do
|
15
|
+
last_modified $updated_at ||= Time.now
|
16
|
+
end
|
17
|
+
|
18
|
+
get '/' do
|
19
|
+
erb :index
|
20
|
+
end
|
21
|
+
|
22
|
+
put '/' do
|
23
|
+
$updated_at = nil
|
24
|
+
redirect '/'
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Sample Rack::Cache Sinatra app</title>
|
4
|
+
<style type="text/css" media="screen">
|
5
|
+
body {
|
6
|
+
font-family: Georgia;
|
7
|
+
font-size: 24px;
|
8
|
+
text-align: center;
|
9
|
+
}
|
10
|
+
|
11
|
+
#headers {
|
12
|
+
font-size: 16px;
|
13
|
+
}
|
14
|
+
|
15
|
+
input {
|
16
|
+
font-size: 24px;
|
17
|
+
cursor: pointer;
|
18
|
+
}
|
19
|
+
</style>
|
20
|
+
</head>
|
21
|
+
<body>
|
22
|
+
<h1>Last updated at: <%= $updated_at.strftime('%l:%m:%S%P') %></h1>
|
23
|
+
|
24
|
+
<p>
|
25
|
+
<form action="/" method="post">
|
26
|
+
<input type="hidden" name="_method" value="PUT">
|
27
|
+
<input type="submit" value="Expire the cache.">
|
28
|
+
</form>
|
29
|
+
</p>
|
30
|
+
|
31
|
+
<div id="headers">
|
32
|
+
<h3>Headers:</h3>
|
33
|
+
|
34
|
+
<% response.headers.each do |key, value| %>
|
35
|
+
<p><%= key %>: <%= value %></p>
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
<h3>Params:</h3>
|
39
|
+
<% params.each do |key, value| %>
|
40
|
+
<p><%= key %>: <%= value || '(blank)' %></p>
|
41
|
+
<% end %>
|
42
|
+
</div>
|
43
|
+
</body>
|
44
|
+
</html>
|