mongrel_esi 0.4.0

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