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.
Files changed (73) hide show
  1. data/COPYING +53 -0
  2. data/LICENSE +471 -0
  3. data/README +186 -0
  4. data/Rakefile +141 -0
  5. data/bin/mongrel_esi +271 -0
  6. data/ext/esi/common.rl +41 -0
  7. data/ext/esi/esi_parser.c +387 -0
  8. data/ext/esi/extconf.rb +6 -0
  9. data/ext/esi/machine.rb +499 -0
  10. data/ext/esi/parser.c +1675 -0
  11. data/ext/esi/parser.h +113 -0
  12. data/ext/esi/parser.rb +49 -0
  13. data/ext/esi/parser.rl +398 -0
  14. data/ext/esi/ruby_esi.rl +135 -0
  15. data/ext/esi/run-test.rb +3 -0
  16. data/ext/esi/test/common.rl +41 -0
  17. data/ext/esi/test/parser.c +1676 -0
  18. data/ext/esi/test/parser.h +113 -0
  19. data/ext/esi/test/parser.rl +398 -0
  20. data/ext/esi/test/test.c +373 -0
  21. data/ext/esi/test1.rb +56 -0
  22. data/ext/esi/test2.rb +45 -0
  23. data/lib/esi/cache.rb +207 -0
  24. data/lib/esi/config.rb +154 -0
  25. data/lib/esi/dispatcher.rb +27 -0
  26. data/lib/esi/handler.rb +236 -0
  27. data/lib/esi/invalidator.rb +40 -0
  28. data/lib/esi/logger.rb +46 -0
  29. data/lib/esi/router.rb +84 -0
  30. data/lib/esi/tag/attempt.rb +6 -0
  31. data/lib/esi/tag/base.rb +85 -0
  32. data/lib/esi/tag/except.rb +24 -0
  33. data/lib/esi/tag/include.rb +190 -0
  34. data/lib/esi/tag/invalidate.rb +54 -0
  35. data/lib/esi/tag/try.rb +40 -0
  36. data/lib/multi_dirhandler.rb +70 -0
  37. data/setup.rb +1585 -0
  38. data/test/integration/basic_test.rb +39 -0
  39. data/test/integration/cache_test.rb +37 -0
  40. data/test/integration/docs/content/500.html +16 -0
  41. data/test/integration/docs/content/500_with_failover.html +16 -0
  42. data/test/integration/docs/content/500_with_failover_to_alt.html +8 -0
  43. data/test/integration/docs/content/ajax_test_page.html +15 -0
  44. data/test/integration/docs/content/cookie_variable.html +3 -0
  45. data/test/integration/docs/content/foo.html +15 -0
  46. data/test/integration/docs/content/include_in_include.html +15 -0
  47. data/test/integration/docs/content/malformed_transforms.html +16 -0
  48. data/test/integration/docs/content/malformed_transforms.html-correct +11 -0
  49. data/test/integration/docs/content/static-failover.html +20 -0
  50. data/test/integration/docs/content/test2.html +1 -0
  51. data/test/integration/docs/content/test3.html +17 -0
  52. data/test/integration/docs/esi_invalidate.html +6 -0
  53. data/test/integration/docs/esi_mixed_content.html +15 -0
  54. data/test/integration/docs/esi_test_content.html +27 -0
  55. data/test/integration/docs/index.html +688 -0
  56. data/test/integration/docs/test1.html +1 -0
  57. data/test/integration/docs/test3.html +9 -0
  58. data/test/integration/docs/test_failover.html +1 -0
  59. data/test/integration/handler_test.rb +270 -0
  60. data/test/integration/help.rb +234 -0
  61. data/test/net/get_test.rb +197 -0
  62. data/test/net/net_helper.rb +16 -0
  63. data/test/net/server_test.rb +249 -0
  64. data/test/unit/base_tag_test.rb +44 -0
  65. data/test/unit/esi-sample.html +56 -0
  66. data/test/unit/help.rb +77 -0
  67. data/test/unit/include_request_test.rb +69 -0
  68. data/test/unit/include_tag_test.rb +14 -0
  69. data/test/unit/parser_test.rb +478 -0
  70. data/test/unit/router_test.rb +34 -0
  71. data/test/unit/sample.html +21 -0
  72. data/tools/rakehelp.rb +119 -0
  73. 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
@@ -0,0 +1,6 @@
1
+ module ESI
2
+ module Tag
3
+ class Attempt < Base
4
+ end
5
+ end
6
+ end
@@ -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