mongrel_esi 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +53 -0
- data/LICENSE +471 -0
- data/README +186 -0
- data/Rakefile +141 -0
- data/bin/mongrel_esi +271 -0
- data/ext/esi/common.rl +41 -0
- data/ext/esi/esi_parser.c +387 -0
- data/ext/esi/extconf.rb +6 -0
- data/ext/esi/machine.rb +499 -0
- data/ext/esi/parser.c +1675 -0
- data/ext/esi/parser.h +113 -0
- data/ext/esi/parser.rb +49 -0
- data/ext/esi/parser.rl +398 -0
- data/ext/esi/ruby_esi.rl +135 -0
- data/ext/esi/run-test.rb +3 -0
- data/ext/esi/test/common.rl +41 -0
- data/ext/esi/test/parser.c +1676 -0
- data/ext/esi/test/parser.h +113 -0
- data/ext/esi/test/parser.rl +398 -0
- data/ext/esi/test/test.c +373 -0
- data/ext/esi/test1.rb +56 -0
- data/ext/esi/test2.rb +45 -0
- data/lib/esi/cache.rb +207 -0
- data/lib/esi/config.rb +154 -0
- data/lib/esi/dispatcher.rb +27 -0
- data/lib/esi/handler.rb +236 -0
- data/lib/esi/invalidator.rb +40 -0
- data/lib/esi/logger.rb +46 -0
- data/lib/esi/router.rb +84 -0
- data/lib/esi/tag/attempt.rb +6 -0
- data/lib/esi/tag/base.rb +85 -0
- data/lib/esi/tag/except.rb +24 -0
- data/lib/esi/tag/include.rb +190 -0
- data/lib/esi/tag/invalidate.rb +54 -0
- data/lib/esi/tag/try.rb +40 -0
- data/lib/multi_dirhandler.rb +70 -0
- data/setup.rb +1585 -0
- data/test/integration/basic_test.rb +39 -0
- data/test/integration/cache_test.rb +37 -0
- data/test/integration/docs/content/500.html +16 -0
- data/test/integration/docs/content/500_with_failover.html +16 -0
- data/test/integration/docs/content/500_with_failover_to_alt.html +8 -0
- data/test/integration/docs/content/ajax_test_page.html +15 -0
- data/test/integration/docs/content/cookie_variable.html +3 -0
- data/test/integration/docs/content/foo.html +15 -0
- data/test/integration/docs/content/include_in_include.html +15 -0
- data/test/integration/docs/content/malformed_transforms.html +16 -0
- data/test/integration/docs/content/malformed_transforms.html-correct +11 -0
- data/test/integration/docs/content/static-failover.html +20 -0
- data/test/integration/docs/content/test2.html +1 -0
- data/test/integration/docs/content/test3.html +17 -0
- data/test/integration/docs/esi_invalidate.html +6 -0
- data/test/integration/docs/esi_mixed_content.html +15 -0
- data/test/integration/docs/esi_test_content.html +27 -0
- data/test/integration/docs/index.html +688 -0
- data/test/integration/docs/test1.html +1 -0
- data/test/integration/docs/test3.html +9 -0
- data/test/integration/docs/test_failover.html +1 -0
- data/test/integration/handler_test.rb +270 -0
- data/test/integration/help.rb +234 -0
- data/test/net/get_test.rb +197 -0
- data/test/net/net_helper.rb +16 -0
- data/test/net/server_test.rb +249 -0
- data/test/unit/base_tag_test.rb +44 -0
- data/test/unit/esi-sample.html +56 -0
- data/test/unit/help.rb +77 -0
- data/test/unit/include_request_test.rb +69 -0
- data/test/unit/include_tag_test.rb +14 -0
- data/test/unit/parser_test.rb +478 -0
- data/test/unit/router_test.rb +34 -0
- data/test/unit/sample.html +21 -0
- data/tools/rakehelp.rb +119 -0
- metadata +182 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# this is a separate thread that runs on port 4001
|
2
|
+
# when requests come to this they must authenticate
|
3
|
+
# and be of type POST
|
4
|
+
# the requests are processed as described by: http://www.w3.org/TR/esi-invp
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
|
8
|
+
module ESI
|
9
|
+
module Invalidator
|
10
|
+
|
11
|
+
def self.start( cache )
|
12
|
+
Thread.new( cache ) do|cache|
|
13
|
+
s = WEBrick::HTTPServer.new( :Port => 4001 )
|
14
|
+
|
15
|
+
s.mount_proc("/invalidate"){|req, res|
|
16
|
+
res.body = "<html>invalidate posted objects</html>"
|
17
|
+
res['Content-Type'] = "text/html"
|
18
|
+
}
|
19
|
+
|
20
|
+
s.mount_proc("/status"){|req, res|
|
21
|
+
res.body = "<html><body><h1>Cached objects</h1>"
|
22
|
+
res.body << "<ul>"
|
23
|
+
cache.keys do|key,data|
|
24
|
+
res.body << "<li>#{key}</li>"
|
25
|
+
end
|
26
|
+
res.body << "</ul>"
|
27
|
+
res.body << "</body>"
|
28
|
+
res.body << "</html>"
|
29
|
+
res['Content-Type'] = "text/html"
|
30
|
+
}
|
31
|
+
|
32
|
+
# XXX: this doesn't chain so ends up removing the mongrel trap locking the server up
|
33
|
+
#trap("INT"){ s.shutdown }
|
34
|
+
s.start
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/esi/logger.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module ESI
|
4
|
+
module Log
|
5
|
+
|
6
|
+
if ENV["test"] == "true"
|
7
|
+
$logger ||= Logger.new("log/test.log")
|
8
|
+
$logger.instance_eval do
|
9
|
+
def puts( msg )
|
10
|
+
debug( msg )
|
11
|
+
end
|
12
|
+
def print( msg )
|
13
|
+
debug( msg )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def log( io, msg )
|
19
|
+
io.puts msg
|
20
|
+
end
|
21
|
+
|
22
|
+
def msg( io, msg )
|
23
|
+
io.print msg
|
24
|
+
end
|
25
|
+
|
26
|
+
def log_request( msg )
|
27
|
+
msg( $logger || STDERR, msg )
|
28
|
+
end
|
29
|
+
|
30
|
+
def log_debug( msg )
|
31
|
+
log( $logger || STDERR, msg )
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_error( msg )
|
35
|
+
log( $logger || STDERR, msg )
|
36
|
+
end
|
37
|
+
|
38
|
+
def log_info( msg )
|
39
|
+
log( $logger || STDERR, msg )
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_warn( msg )
|
43
|
+
log( $logger || STDERR, msg )
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/esi/router.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module ESI
|
2
|
+
|
3
|
+
# ESI Server is a reverse proxy caching server. It will forward all requests to app servers
|
4
|
+
# by processing a config_file that specifies routes:
|
5
|
+
#
|
6
|
+
# content:
|
7
|
+
# host: 127.0.0.1
|
8
|
+
# port: 3000
|
9
|
+
# match_url: ^\/(content|samples|extra).*
|
10
|
+
# default:
|
11
|
+
# host: 127.0.0.1
|
12
|
+
# port: 3001
|
13
|
+
#
|
14
|
+
# This sample configuration will route all urls starting with /content, /samples, or /extras
|
15
|
+
# to the server running at 127.0.0.1 on port 3000
|
16
|
+
# everything else that matches the .* will be routed to the server running on port 3001
|
17
|
+
# optionally the caching duration can be specificied explicity for each host, this will be the default per host
|
18
|
+
# if the esi:include tag does not specify otherwise
|
19
|
+
#
|
20
|
+
# default:
|
21
|
+
# host: 127.0.01
|
22
|
+
# port: 3001
|
23
|
+
# cache_ttl: 300
|
24
|
+
#
|
25
|
+
# This example will cache all requests for 300 seconds, by default.
|
26
|
+
#
|
27
|
+
# To create a router load either from memory or file the above YAML
|
28
|
+
#
|
29
|
+
# router = ESI::Router.new( YAML.load_file('config.yml') )
|
30
|
+
#
|
31
|
+
# or from memory
|
32
|
+
#
|
33
|
+
# router = ESI::Router.new( YAML.load(config_str) )
|
34
|
+
#
|
35
|
+
class Router
|
36
|
+
|
37
|
+
# config is a routing table as defined above
|
38
|
+
def initialize( routes )
|
39
|
+
@hosts = []
|
40
|
+
@default = nil
|
41
|
+
|
42
|
+
routes.each do|cfg|
|
43
|
+
|
44
|
+
raise "Configuration error missing host for #{cfg.inspect}" if !cfg[:host]
|
45
|
+
raise "Configuration error missing port for #{cfg.inspect}" if !cfg[:port]
|
46
|
+
raise "Configuration error missing match_url for #{cfg.inspect}" if !cfg[:match_url]
|
47
|
+
|
48
|
+
if cfg[:match_url] == 'default'
|
49
|
+
@default = cfg
|
50
|
+
else
|
51
|
+
raise "Configuration error missing match_url for #{cfg.inspect}" if !cfg[:match_url]
|
52
|
+
@hosts << cfg
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
@default = {:host => '127.0.0.1', :port => '3000'} unless @default
|
58
|
+
end
|
59
|
+
|
60
|
+
# return a uri given a request_uri
|
61
|
+
def url_for( request_uri )
|
62
|
+
return request_uri if request_uri.match(/http:\/\//)
|
63
|
+
# locate the first entry to match the given uri
|
64
|
+
config = @hosts.find do |cfg|
|
65
|
+
Regexp.new(cfg[:match_url]).match(request_uri)
|
66
|
+
end
|
67
|
+
config = @default unless config
|
68
|
+
# build a url given a valid config or abort 404 not found
|
69
|
+
"http://" + (config[:host] + ":" + (config[:port] || "").to_s + "/" + request_uri).squeeze("/")
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache_ttl( request_uri )
|
73
|
+
return 600 if request_uri.match(/http:\/\//) # default cache for absolute URLs
|
74
|
+
# locate the first entry to match the given uri
|
75
|
+
config = @hosts.find do |cfg|
|
76
|
+
Regexp.new(cfg[:match_url]).match(request_uri)
|
77
|
+
end
|
78
|
+
config = @default unless config
|
79
|
+
(config[:cache_ttl] || 600).to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
data/lib/esi/tag/base.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'esi/logger'
|
2
|
+
|
3
|
+
module ESI
|
4
|
+
module Tag
|
5
|
+
class Base
|
6
|
+
Validate = ['try','attempt','except','include','invalidate']
|
7
|
+
attr_reader :attributes, :children, :name, :closed, :cache
|
8
|
+
|
9
|
+
include ESI::Log
|
10
|
+
extend ESI::Log
|
11
|
+
|
12
|
+
def initialize(router,headers,http_params,name,attrs,cache)
|
13
|
+
@router = router
|
14
|
+
@headers = headers
|
15
|
+
@http_params = http_params
|
16
|
+
@attributes = attrs
|
17
|
+
@cache = cache
|
18
|
+
@children = []
|
19
|
+
@name = name
|
20
|
+
@unclosed = nil
|
21
|
+
@closed = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create(router,headers,http_params,tag_name,attrs,cache)
|
25
|
+
tag_name.gsub!(/esi:/,'')
|
26
|
+
raise "Unsupport ESI tag: #{tag_name}" unless Validate.include?(tag_name)
|
27
|
+
eval(tag_name.capitalize).new(router,headers,http_params,tag_name,attrs,cache)
|
28
|
+
rescue => e
|
29
|
+
log_debug "Failed while creating tag: #{tag_name}, with error: #{e.message}"
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
def buffer( output, inner_html )
|
34
|
+
if output.respond_to?("<<")
|
35
|
+
output << inner_html
|
36
|
+
else
|
37
|
+
output.call inner_html
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def close( output, options = {} )
|
42
|
+
@closed = true
|
43
|
+
end
|
44
|
+
|
45
|
+
def close_child( output, name )
|
46
|
+
name = name.gsub(/esi:/,'')
|
47
|
+
if @unclosed and @unclosed.name == name
|
48
|
+
@unclosed.close( output )
|
49
|
+
@unclosed = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_child(tag)
|
54
|
+
if @unclosed
|
55
|
+
@unclosed.add_child(tag)
|
56
|
+
else
|
57
|
+
@unclosed = tag
|
58
|
+
@children << tag
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# :startdoc:
|
63
|
+
# scans the fragment url, replacing occurrances of $(VAR{ .. with the given value read from
|
64
|
+
# the HTTP Request object
|
65
|
+
# :enddoc:
|
66
|
+
def prepare_url_vars(url)
|
67
|
+
# scan url for $(VAR{
|
68
|
+
# using the regex look for url vars that get set via http params
|
69
|
+
# for each var extract the value and store it in the operations array
|
70
|
+
operations = []
|
71
|
+
url.scan(/\$\([0-9a-zA-Z_{}]+\)/x) do|var|
|
72
|
+
# extract the var name
|
73
|
+
name = var.gsub(/\$\(/,'').gsub(/\{.*$/,'')
|
74
|
+
key = var.gsub(/^.*\{/,'').gsub(/\}.*$/,'')
|
75
|
+
value = Mongrel::HttpRequest.query_parse(@headers[name])[key]
|
76
|
+
operations << {:sub => var, :with => (value||"")}
|
77
|
+
end
|
78
|
+
# apply each operation to the url
|
79
|
+
operations.each { |op| url.gsub!(op[:sub],op[:with]) }
|
80
|
+
url
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ESI
|
2
|
+
module Tag
|
3
|
+
class Except < Base
|
4
|
+
attr_reader :buffers
|
5
|
+
|
6
|
+
def initialize(uri,headers,http_params,name,attrs,cache)
|
7
|
+
super
|
8
|
+
@buffers = [] # buffer output since this may only appear if the attempt block fails first
|
9
|
+
@buffer_index = 0
|
10
|
+
@buffers[@buffer_index] = ""
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_child(tag)
|
14
|
+
super(tag)
|
15
|
+
@buffer_index += 1
|
16
|
+
@buffers[@buffer_index] = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def buffer( output, inner_html )
|
20
|
+
@buffers[@buffer_index] << inner_html
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module ESI
|
5
|
+
module Tag
|
6
|
+
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# ir = IncludeRequest.new( {'header1'=>'value1'} )
|
10
|
+
#
|
11
|
+
# ir.request( '/fragment' ) do|status,response|
|
12
|
+
# if status
|
13
|
+
# response.read_body do|str|
|
14
|
+
# end
|
15
|
+
# else
|
16
|
+
# # error case
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
class IncludeRequest
|
21
|
+
class Error
|
22
|
+
attr_reader :message, :response
|
23
|
+
def initialize(msg,response)
|
24
|
+
@message = msg
|
25
|
+
@response = response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
attr_reader :exception, :overflow_index # TODO
|
29
|
+
|
30
|
+
def initialize(forward_headers)
|
31
|
+
@headers = forward_headers
|
32
|
+
end
|
33
|
+
|
34
|
+
def request(uri, timeout = 1, alt_failover=nil, follow_limit=3)
|
35
|
+
uri = URI.parse(uri) if uri.is_a?(String)
|
36
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
37
|
+
http.read_timeout = timeout
|
38
|
+
http.get2( uri.to_s, @headers ) do|response|
|
39
|
+
case response
|
40
|
+
when Net::HTTPSuccess
|
41
|
+
yield true, response, uri
|
42
|
+
when Net::HTTPRedirection
|
43
|
+
ir = IncludeRequest.new(@headers)
|
44
|
+
ir.request(URI.parse(response['location']), timeout, alt_failover, follow_limit - 1) do|s,r|
|
45
|
+
yield s, r, URI.parse(response['location'])
|
46
|
+
end
|
47
|
+
else
|
48
|
+
if alt_failover
|
49
|
+
ir = IncludeRequest.new(@headers)
|
50
|
+
ir.request(alt_failover, timeout, nil, follow_limit) do|s,r|
|
51
|
+
yield s, r, URI.parse(alt_failover)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
yield false, Error.new("Failed to request fragment: #{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}", response), uri
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue Timeout::Error => e
|
59
|
+
yield false, Error.new("Failed to request fragment: #{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}, timeout error: #{e.message}", nil), uri
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class Include < Base
|
65
|
+
|
66
|
+
attr_accessor :depth, :max_depth
|
67
|
+
def initialize(uri,headers,http_params,name,attrs,cache)
|
68
|
+
super
|
69
|
+
@depth = 0
|
70
|
+
@max_depth = 3
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_fragment?
|
74
|
+
@depth <= @max_depth
|
75
|
+
end
|
76
|
+
|
77
|
+
def close( output, options = {} )
|
78
|
+
super(output)
|
79
|
+
|
80
|
+
raise_on_error = options[:raise] || false
|
81
|
+
ir = IncludeRequest.new(@http_params)
|
82
|
+
|
83
|
+
src = @router.url_for(prepare_url_vars(@attributes["src"]))
|
84
|
+
alt = @attributes['alt']
|
85
|
+
alt = @router.url_for(prepare_url_vars(alt)) if alt
|
86
|
+
|
87
|
+
parser = nil
|
88
|
+
|
89
|
+
if parse_fragment?
|
90
|
+
parser = ESI::CParser.new
|
91
|
+
parser.output = output
|
92
|
+
parser.depth = (@depth+1)
|
93
|
+
|
94
|
+
# NOTE: really bad things happen if we attempt to copy the closure from the main parser
|
95
|
+
# in esi/handler.rb
|
96
|
+
|
97
|
+
# handle start tags
|
98
|
+
parser.start_tag_handler do|tag_name, attrs|
|
99
|
+
tag = ESI::Tag::Base.create( @router,
|
100
|
+
@headers,
|
101
|
+
@http_params,
|
102
|
+
tag_name.gsub(/esi:/,''),
|
103
|
+
attrs,
|
104
|
+
@cache )
|
105
|
+
# set the tag depth
|
106
|
+
tag.depth = (@depth+1) if tag.respond_to?(:depth=)
|
107
|
+
tag.max_depth = @max_depth if tag.respond_to?(:max_depth=)
|
108
|
+
|
109
|
+
if parser.esi_tag
|
110
|
+
parser.esi_tag.add_child(tag)
|
111
|
+
else
|
112
|
+
parser.esi_tag = tag
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# handle end tags
|
117
|
+
parser.end_tag_handler do|tag_name|
|
118
|
+
if parser.esi_tag.name == tag_name.gsub(/esi:/,'')
|
119
|
+
parser.esi_tag.close(parser.output)
|
120
|
+
parser.esi_tag = nil
|
121
|
+
else
|
122
|
+
parser.esi_tag.close_child(parser.output,tag_name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
if @cache.cached?( src, @http_params )
|
130
|
+
cached_fragment = @cache.get( src, @headers ).body
|
131
|
+
log_request "C"
|
132
|
+
if parse_fragment?
|
133
|
+
|
134
|
+
parser.process cached_fragment
|
135
|
+
parser.finish
|
136
|
+
|
137
|
+
else
|
138
|
+
output << cached_fragment
|
139
|
+
end
|
140
|
+
|
141
|
+
else
|
142
|
+
|
143
|
+
ir.request(src, @attributes['timeout'].to_i, alt ) do|status,response,uri|
|
144
|
+
if status
|
145
|
+
# NOTE: it's important that we cache the unprocessed markup, because we need to
|
146
|
+
# reprocess the esi:include vars even for cached content, this way we can have cached content
|
147
|
+
# with HTTP_COOKIE vars and avoid re-requesting content
|
148
|
+
log_request "R"
|
149
|
+
cache_buffer = ""
|
150
|
+
response.read_body do|s|
|
151
|
+
cache_buffer << s
|
152
|
+
if parse_fragment?
|
153
|
+
parser.process s
|
154
|
+
else
|
155
|
+
output << s
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
parser.finish if parse_fragment?
|
160
|
+
|
161
|
+
if src != uri # these won't be equal if the fragment followed a redirect or used the alt condition
|
162
|
+
if uri.query
|
163
|
+
request_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}?#{uri.query}"
|
164
|
+
else
|
165
|
+
request_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}"
|
166
|
+
end
|
167
|
+
else
|
168
|
+
request_uri = src
|
169
|
+
end
|
170
|
+
|
171
|
+
@cache.put(request_uri, @http_params, 600, cache_buffer )
|
172
|
+
|
173
|
+
else
|
174
|
+
# error/ check if the include has an onerror specifier
|
175
|
+
return if @attributes['onerror'] == 'continue'
|
176
|
+
# response is an IncludeRequest::Error
|
177
|
+
raise response.message if raise_on_error
|
178
|
+
# stop processing and return the error object
|
179
|
+
return response
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
parser = nil
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|