mongrel_esi 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|