rack_ssi 0.0.2

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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-ssi.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "rake"
8
+ gem "rspec"
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Thibaut Sacreste
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Rack::SSI
2
+
3
+ Rack middleware for processing SSI based on the [nginx HttpSsiModule](http://wiki.nginx.org/HttpSsiModule).
4
+ Directives currently supported: `block` and `include`
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'rack_ssi', :git => "git@github.com:forward/rack-ssi.git"
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install rack_ssi
19
+
20
+ ## Usage
21
+
22
+ require 'rack_ssi'
23
+
24
+ ### Sinatra
25
+
26
+ configure do
27
+ use Rack::SSI, {
28
+ :logging => :on,
29
+ :when => lambda {|env| not env['SOME_CUSTOM_HEADER'] == 'ON'},
30
+ :locations => {
31
+ %r{^/includes} => "http://includes.mydomain.com"
32
+ }
33
+ }
34
+ end
35
+
36
+ ### Rails
37
+
38
+ config.middleware.use Rack::SSI, { ... }
39
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
data/lib/rack_ssi.rb ADDED
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../ssi_processor', __FILE__)
2
+ require 'rest_client'
3
+ require 'logger'
4
+
5
+ module Rack
6
+ class SSI
7
+
8
+ def initialize(app, options = {})
9
+ @app = app
10
+ @logging = options[:logging] == :on
11
+ @locations = options[:locations] || {}
12
+ @predicate = options[:when] || lambda {|_| true}
13
+ end
14
+
15
+ def call(env)
16
+ status, headers, body = @app.call(env)
17
+ unprocessed = [status, headers, body]
18
+
19
+ return unprocessed unless @predicate.call(env)
20
+ return unprocessed unless headers["Content-Type"] && headers["Content-Type"].include?("text/html")
21
+ return unprocessed unless status == 200
22
+
23
+ ssi = Rack::SSIProcessor.new
24
+ ssi.locations = @locations
25
+ ssi.logger = logger(env) if @logging
26
+ new_body = ssi.process(body)
27
+ headers["Content-Length"] = (new_body.reduce(0) {|sum, part| sum + part.bytesize}).to_s
28
+
29
+ [status, headers, new_body]
30
+ end
31
+
32
+ private
33
+ def logger(env)
34
+ if defined?(Rails)
35
+ return Rails.logger
36
+ end
37
+ env['rack.logger']
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,69 @@
1
+ module Rack
2
+ class SSIProcessor
3
+
4
+ attr_accessor :logger, :locations
5
+
6
+ def process(body)
7
+ # see http://wiki.nginx.org/HttpSsiModule
8
+ # currently only supporting 'block' and 'include' directives
9
+ blocks = {}
10
+ output = []
11
+ body.each do |part|
12
+ new_part = process_block(part) {|name, content| blocks[name] = content}
13
+ output << process_include(new_part, blocks)
14
+ end
15
+ output
16
+ end
17
+
18
+ def process_block(part)
19
+ part.gsub(/<!--#\s+block\s+name="(\w+)"\s+-->(.*?)<!--#\s+endblock\s+-->/) do
20
+ name, content = $1, $2
21
+ _info "processing block directive with name=#{name}"
22
+ yield [name, content]
23
+ ""
24
+ end
25
+ end
26
+
27
+ def process_include(part, blocks)
28
+ part.gsub(/<!--#\s+include\s+(?:virtual|file)="([^"]+)"(?:\s+stub="(\w+)")?\s+-->/) do
29
+ location, stub = $1, $2
30
+ _info "processing include directive with location=#{location}"
31
+ status, _, body = fetch location
32
+ if stub && (status != 200 || body.nil? || body == "")
33
+ blocks[stub]
34
+ else
35
+ body
36
+ end
37
+ end
38
+ end
39
+
40
+ def fetch(location)
41
+ locations.select{|k,v| k.is_a?(String)}.each do |pattern, host|
42
+ return _get("#{host}#{location}") if location == pattern
43
+ end
44
+ locations.select{|k,v| k.is_a?(Regexp)}.each do |pattern, host|
45
+ return _get("#{host}#{location}") if location =~ pattern
46
+ end
47
+ _error "no match found for location=#{location}"
48
+ end
49
+
50
+ private
51
+
52
+ def _get(url)
53
+ _info "fetching #{url}"
54
+ RestClient.get(url) do |response, request, result|
55
+ _error "error fetching #{url}: #{response.code} response" if response.code != 200
56
+ [response.code, response.headers, response.body]
57
+ end
58
+ end
59
+
60
+ def _info(message)
61
+ logger.info "Rack::SSI #{message}" if logger
62
+ end
63
+
64
+ def _error(message)
65
+ logger.info "Rack::SSI #{message}" if logger
66
+ end
67
+
68
+ end
69
+ end
data/rack-ssi.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["Thibaut Sacreste"]
5
+ gem.email = ["thibaut.sacreste@gmail.com"]
6
+ gem.description = <<-EOS
7
+ Rack middleware for processing SSI based on the nginx HttpSsiModule.
8
+ Directives currently supported: 'block' and 'include'
9
+ EOS
10
+ gem.summary = "Rack middleware for processing SSI based on the nginx HttpSsiModule."
11
+ gem.homepage = "https://github.com/forward/rack-ssi"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "rack_ssi"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = "0.0.2"
19
+ gem.add_dependency "rest-client"
20
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Rack::SSI do
4
+ describe "#call" do
5
+
6
+ it "should not process response if the when predicate returns false" do
7
+ app = double
8
+ body = ["I am a response"]
9
+ app.stub(:call).and_return([200, {"Content-Type" => ["text/html"]}, body])
10
+ rack_ssi = Rack::SSI.new(app, :when => lambda {|env| false })
11
+
12
+ Rack::SSIProcessor.any_instance.should_not_receive(:process).and_return([""])
13
+
14
+ rack_ssi.call({})
15
+ end
16
+
17
+ it "should process response if the when predicate returns true" do
18
+ app = double
19
+ body = ["I am a response"]
20
+ app.stub(:call).and_return([200, {"Content-Type" => ["text/html"]}, body])
21
+ rack_ssi = Rack::SSI.new(app, :when => lambda {|env| true })
22
+
23
+ Rack::SSIProcessor.any_instance.should_receive(:process).with(body).once.and_return([""])
24
+
25
+ rack_ssi.call({})
26
+ end
27
+
28
+ it "should process html responses only" do
29
+ app = double
30
+ body = ["I am a response"]
31
+ app.stub(:call).and_return(
32
+ [200, {"Content-Type" => ["text/html"]}, body],
33
+ [200, {"Content-Type" => ["text/css"]}, []])
34
+ rack_ssi = Rack::SSI.new(app)
35
+
36
+ Rack::SSIProcessor.any_instance.should_receive(:process).with(body).once.and_return([""])
37
+
38
+ rack_ssi.call({})
39
+ end
40
+
41
+ it "should process 200 responses only" do
42
+ app = double
43
+ body = ["I am a response"]
44
+ app.stub(:call).and_return(
45
+ [200, {"Content-Type" => ["text/html"]}, body],
46
+ [500, {"Content-Type" => ["text/html"]}, []])
47
+ rack_ssi = Rack::SSI.new(app)
48
+
49
+ Rack::SSIProcessor.any_instance.should_receive(:process).with(body).once.and_return([""])
50
+
51
+ rack_ssi.call({})
52
+ end
53
+
54
+ it "should return the processed response and update the Content-Length header" do
55
+ body = ["I am a response"]
56
+ app = double(:call => [200, {"Content-Type" => ["text/html"], "Content-Length" => "15"}, body])
57
+ rack_ssi = Rack::SSI.new(app)
58
+ Rack::SSIProcessor.any_instance.stub(:process => ["I am a bigger response"])
59
+
60
+ _, headers, new_body = rack_ssi.call({})
61
+
62
+ new_body.should == ["I am a bigger response"]
63
+ headers["Content-Length"].should == "22"
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path('../../lib/rack_ssi', __FILE__)
@@ -0,0 +1,197 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Rack::SSIProcessor do
4
+
5
+ describe "#process_block" do
6
+ it "should yield block directives and strip them out of the html" do
7
+ html = <<-eos
8
+ <html>
9
+ <body>
10
+ <!--# block name="shush" --><!--# endblock -->
11
+ <p>some content</p>
12
+ <!--# block name="shouty" --><h1>ERROR!</h1><!--# endblock -->
13
+ </body>
14
+ </html>
15
+ eos
16
+
17
+ expected = <<-eos.gsub /\s+/, ""
18
+ <html>
19
+ <body>
20
+ <p>some content</p>
21
+ </body>
22
+ </html>
23
+ eos
24
+
25
+ ssi = Rack::SSIProcessor.new
26
+ blocks = []
27
+
28
+ processed = ssi.process_block(html) {|block| blocks << block}
29
+
30
+ processed.gsub(/\s+/, "").should == expected
31
+ blocks.should == [["shush", ""], ["shouty", "<h1>ERROR!</h1>"]]
32
+ end
33
+ end
34
+
35
+ describe "#process_include" do
36
+ context "the SSI include request returns a valid response" do
37
+ it "should replace include directives with appropriate content" do
38
+ html = <<-eos
39
+ <html>
40
+ <body>
41
+ <!--# include virtual="/some/location" -->
42
+ <!--# include virtual="/some/other/location" -->
43
+ </body>
44
+ </html>
45
+ eos
46
+
47
+ expected = <<-eos.gsub /\s+/, ""
48
+ <html>
49
+ <body>
50
+ <p>some content</p>
51
+ <p>some more content</p>
52
+ </body>
53
+ </html>
54
+ eos
55
+
56
+ ssi = Rack::SSIProcessor.new
57
+ ssi.stub(:fetch).with("/some/location").and_return([200, {}, "<p>some content</p>"])
58
+ ssi.stub(:fetch).with("/some/other/location").and_return([200, {}, "<p>some more content</p>"])
59
+
60
+ processed = ssi.process_include(html, {})
61
+
62
+ processed.gsub(/\s+/, "").should == expected
63
+ end
64
+ end
65
+
66
+ context "the SSI include request returns an empty response" do
67
+ it "should replace include directives with the content of the block specified by the 'stub' parameter" do
68
+ html = <<-eos
69
+ <html>
70
+ <body>
71
+ <!--# include virtual="/some/broken/location" stub="oops" -->
72
+ </body>
73
+ </html>
74
+ eos
75
+
76
+ expected = <<-eos.gsub /\s+/, ""
77
+ <html>
78
+ <body>
79
+ <p>oops, something went wrong!</p>
80
+ </body>
81
+ </html>
82
+ eos
83
+
84
+ ssi = Rack::SSIProcessor.new
85
+ ssi.stub(:fetch).with("/some/broken/location").and_return([200, {}, ""])
86
+
87
+ processed = ssi.process_include(html, {"oops" => "<p>oops, something went wrong!</p>"})
88
+
89
+ processed.gsub(/\s+/, "").should == expected
90
+ end
91
+
92
+ it "should replace include directives with the empty response if no 'stub' parameter" do
93
+ html = <<-eos
94
+ <html>
95
+ <body>
96
+ <!--# include virtual="/some/broken/location" -->
97
+ </body>
98
+ </html>
99
+ eos
100
+
101
+ ssi = Rack::SSIProcessor.new
102
+ ssi.stub(:fetch).with("/some/broken/location").and_return([200, {}, ""])
103
+
104
+ processed = ssi.process_include(html, {})
105
+
106
+ processed.gsub(/\s+/, "").should == "<html><body></body></html>"
107
+ end
108
+ end
109
+
110
+ context "the SSI include request returns an error response" do
111
+ it "should replace include directives with the content of the block specified by the 'stub' parameter" do
112
+ html = <<-eos
113
+ <html>
114
+ <body>
115
+ <!--# include virtual="/some/broken/location" stub="oops" -->
116
+ </body>
117
+ </html>
118
+ eos
119
+
120
+ expected = <<-eos.gsub /\s+/, ""
121
+ <html>
122
+ <body>
123
+ <p>oops, something went wrong!</p>
124
+ </body>
125
+ </html>
126
+ eos
127
+
128
+ ssi = Rack::SSIProcessor.new
129
+ ssi.stub(:fetch).with("/some/broken/location").and_return([500, {}, "<crap>"])
130
+
131
+ processed = ssi.process_include(html, {"oops" => "<p>oops, something went wrong!</p>"})
132
+
133
+ processed.gsub(/\s+/, "").should == expected
134
+ end
135
+
136
+ it "should replace include directives with the error response if no 'stub' parameter" do
137
+ html = <<-eos
138
+ <html>
139
+ <body>
140
+ <!--# include virtual="/some/broken/location" -->
141
+ </body>
142
+ </html>
143
+ eos
144
+
145
+ ssi = Rack::SSIProcessor.new
146
+ ssi.stub(:fetch).with("/some/broken/location").and_return([500, {}, "<bang>"])
147
+
148
+ processed = ssi.process_include(html, {})
149
+
150
+ processed.gsub(/\s+/, "").should == "<html><body><bang></body></html>"
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ describe "#fetch" do
157
+ it "should resolve locations by exact match first" do
158
+ ssi = Rack::SSIProcessor.new
159
+ ssi.locations = {
160
+ /\/pants/ => "http://host1",
161
+ "/pants" => "http://host2"
162
+ }
163
+
164
+ RestClient.should_receive(:get).with("http://host2/pants")
165
+ ssi.fetch("/pants")
166
+ end
167
+
168
+ it "should resolve locations by regex if no exact match" do
169
+ ssi = Rack::SSIProcessor.new
170
+ ssi.locations = {
171
+ /^\/pants\/.*/ => "http://host1",
172
+ "/pants" => "http://host2"
173
+ }
174
+
175
+ RestClient.should_receive(:get).with("http://host1/pants/on/fire")
176
+ ssi.fetch("/pants/on/fire")
177
+ end
178
+ end
179
+
180
+ describe "#process" do
181
+ it "should do it all!" do
182
+ body = [
183
+ '<html><body>',
184
+ '<!--# block name="shouty" --><p>ERROR!</p><!--# endblock -->',
185
+ '<!--# include virtual="/includes/broken" stub="shouty" -->',
186
+ '<!--# include virtual="/includes/header" -->',
187
+ '</body></html>'
188
+ ]
189
+ ssi = Rack::SSIProcessor.new
190
+ ssi.stub(:fetch).with("/includes/broken").and_return([500, {}, "<p>pants!</p>"])
191
+ ssi.stub(:fetch).with("/includes/header").and_return([200, {}, "<h1>Hello</h1>"])
192
+
193
+ ssi.process(body).join.should == "<html><body><p>ERROR!</p><h1>Hello</h1></body></html>"
194
+ end
195
+ end
196
+
197
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack_ssi
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Thibaut Sacreste
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-20 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rest-client
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: " Rack middleware for processing SSI based on the nginx HttpSsiModule.\n Directives currently supported: 'block' and 'include'\n"
35
+ email:
36
+ - thibaut.sacreste@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - LICENSE
47
+ - README.md
48
+ - Rakefile
49
+ - lib/rack_ssi.rb
50
+ - lib/ssi_processor.rb
51
+ - rack-ssi.gemspec
52
+ - spec/rack_ssi_spec.rb
53
+ - spec/spec_helper.rb
54
+ - spec/ssi_processor_spec.rb
55
+ homepage: https://github.com/forward/rack-ssi
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.24
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Rack middleware for processing SSI based on the nginx HttpSsiModule.
88
+ test_files:
89
+ - spec/rack_ssi_spec.rb
90
+ - spec/spec_helper.rb
91
+ - spec/ssi_processor_spec.rb