summary refs log tree commit
diff options
context:
space:
mode:
authorJoshua Peek <josh@joshpeek.com>2009-12-11 16:03:39 -0600
committerJoshua Peek <josh@joshpeek.com>2009-12-11 16:03:39 -0600
commit981f182bcfa1b848aa9e66c72500d855f6ee77ff (patch)
treeaf921775f8bc453c028f41570602b1c5f62729e5
parent37195bedbc6d1f02a47fea5712ad792aad5c1d4b (diff)
downloadrack-981f182bcfa1b848aa9e66c72500d855f6ee77ff.tar.gz
Import Ryan's Sendfile from contrib into core
-rw-r--r--lib/rack.rb1
-rw-r--r--lib/rack/sendfile.rb142
-rw-r--r--test/spec_rack_sendfile.rb86
3 files changed, 229 insertions, 0 deletions
diff --git a/lib/rack.rb b/lib/rack.rb
index bb1e4ed1..5f0e797b 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -45,6 +45,7 @@ module Rack
   autoload :Recursive, "rack/recursive"
   autoload :Reloader, "rack/reloader"
   autoload :Runtime, "rack/runtime"
+  autoload :Sendfile, "rack/sendfile"
   autoload :Server, "rack/server"
   autoload :ShowExceptions, "rack/showexceptions"
   autoload :ShowStatus, "rack/showstatus"
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
new file mode 100644
index 00000000..4fa82946
--- /dev/null
+++ b/lib/rack/sendfile.rb
@@ -0,0 +1,142 @@
+require 'rack/file'
+
+module Rack
+  class File #:nodoc:
+    alias :to_path :path
+  end
+
+  # = Sendfile
+  #
+  # The Sendfile middleware intercepts responses whose body is being
+  # served from a file and replaces it with a server specific X-Sendfile
+  # header. The web server is then responsible for writing the file contents
+  # to the client. This can dramatically reduce the amount of work required
+  # by the Ruby backend and takes advantage of the web servers optimized file
+  # delivery code.
+  #
+  # In order to take advantage of this middleware, the response body must
+  # respond to +to_path+ and the request must include an X-Sendfile-Type
+  # header. Rack::File and other components implement +to_path+ so there's
+  # rarely anything you need to do in your application. The X-Sendfile-Type
+  # header is typically set in your web servers configuration. The following
+  # sections attempt to document
+  #
+  # === Nginx
+  #
+  # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
+  # but requires parts of the filesystem to be mapped into a private URL
+  # hierarachy.
+  #
+  # The following example shows the Nginx configuration required to create
+  # a private "/files/" area, enable X-Accel-Redirect, and pass the special
+  # X-Sendfile-Type and X-Accel-Mapping headers to the backend:
+  #
+  #   location /files/ {
+  #     internal;
+  #     alias /var/www/;
+  #   }
+  #
+  #   location / {
+  #     proxy_redirect     false;
+  #
+  #     proxy_set_header   Host                $host;
+  #     proxy_set_header   X-Real-IP           $remote_addr;
+  #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
+  #
+  #     proxy_set_header   X-Sendfile-Type     X-Accel-Redirect
+  #     proxy_set_header   X-Accel-Mapping     /files/=/var/www/;
+  #
+  #     proxy_pass         http://127.0.0.1:8080/;
+  #   }
+  #
+  # Note that the X-Sendfile-Type header must be set exactly as shown above. The
+  # X-Accel-Mapping header should specify the name of the private URL pattern,
+  # followed by an equals sign (=), followed by the location on the file system
+  # that it maps to. The middleware performs a simple substitution on the
+  # resulting path.
+  #
+  # See Also: http://wiki.codemongers.com/NginxXSendfile
+  #
+  # === lighttpd
+  #
+  # Lighttpd has supported some variation of the X-Sendfile header for some
+  # time, although only recent version support X-Sendfile in a reverse proxy
+  # configuration.
+  #
+  #   $HTTP["host"] == "example.com" {
+  #      proxy-core.protocol = "http"
+  #      proxy-core.balancer = "round-robin"
+  #      proxy-core.backends = (
+  #        "127.0.0.1:8000",
+  #        "127.0.0.1:8001",
+  #        ...
+  #      )
+  #
+  #      proxy-core.allow-x-sendfile = "enable"
+  #      proxy-core.rewrite-request = (
+  #        "X-Sendfile-Type" => (".*" => "X-Sendfile")
+  #      )
+  #    }
+  #
+  # See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
+  #
+  # === Apache
+  #
+  # X-Sendfile is supported under Apache 2.x using a separate module:
+  #
+  # http://tn123.ath.cx/mod_xsendfile/
+  #
+  # Once the module is compiled and installed, you can enable it using
+  # XSendFile config directive:
+  #
+  #   RequestHeader Set X-Sendfile-Type X-Sendfile
+  #   ProxyPassReverse / http://localhost:8001/
+  #   XSendFile on
+
+  class Sendfile
+    F = ::File
+
+    def initialize(app, variation=nil)
+      @app = app
+      @variation = variation
+    end
+
+    def call(env)
+      status, headers, body = @app.call(env)
+      if body.respond_to?(:to_path)
+        case type = variation(env)
+        when 'X-Accel-Redirect'
+          path = F.expand_path(body.to_path)
+          if url = map_accel_path(env, path)
+            headers[type] = url
+            body = []
+          else
+            env['rack.errors'] << "X-Accel-Mapping header missing"
+          end
+        when 'X-Sendfile', 'X-Lighttpd-Send-File'
+          path = F.expand_path(body.to_path)
+          headers[type] = path
+          body = []
+        when '', nil
+        else
+          env['rack.errors'] << "Unknown x-sendfile variation: '#{variation}'.\n"
+        end
+      end
+      [status, headers, body]
+    end
+
+    private
+      def variation(env)
+        @variation ||
+          env['sendfile.type'] ||
+          env['HTTP_X_SENDFILE_TYPE']
+      end
+
+      def map_accel_path(env, file)
+        if mapping = env['HTTP_X_ACCEL_MAPPING']
+          internal, external = mapping.split('=', 2).map{ |p| p.strip }
+          file.sub(/^#{internal}/i, external)
+        end
+      end
+  end
+end
diff --git a/test/spec_rack_sendfile.rb b/test/spec_rack_sendfile.rb
new file mode 100644
index 00000000..8cfe2017
--- /dev/null
+++ b/test/spec_rack_sendfile.rb
@@ -0,0 +1,86 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/sendfile'
+
+context "Rack::File" do
+  specify "should respond to #to_path" do
+    Rack::File.new(Dir.pwd).should.respond_to :to_path
+  end
+end
+
+context "Rack::Sendfile" do
+  def sendfile_body
+    res = ['Hello World']
+    def res.to_path ; "/tmp/hello.txt" ; end
+    res
+  end
+
+  def simple_app(body=sendfile_body)
+    lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+  end
+
+  def sendfile_app(body=sendfile_body)
+    Rack::Sendfile.new(simple_app(body))
+  end
+
+  setup do
+    @request = Rack::MockRequest.new(sendfile_app)
+  end
+
+  def request(headers={})
+    yield @request.get('/', headers)
+  end
+
+  specify "does nothing when no X-Sendfile-Type header present" do
+    request do |response|
+      response.should.be.ok
+      response.body.should.equal 'Hello World'
+      response.headers.should.not.include 'X-Sendfile'
+    end
+  end
+
+  specify "sets X-Sendfile response header and discards body" do
+    request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+      response.should.be.ok
+      response.body.should.be.empty
+      response.headers['X-Sendfile'].should.equal '/tmp/hello.txt'
+    end
+  end
+
+  specify "sets X-Lighttpd-Send-File response header and discards body" do
+    request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response|
+      response.should.be.ok
+      response.body.should.be.empty
+      response.headers['X-Lighttpd-Send-File'].should.equal '/tmp/hello.txt'
+    end
+  end
+
+  specify "sets X-Accel-Redirect response header and discards body" do
+    headers = {
+      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+      'HTTP_X_ACCEL_MAPPING' => '/tmp/=/foo/bar/'
+    }
+    request headers do |response|
+      response.should.be.ok
+      response.body.should.be.empty
+      response.headers['X-Accel-Redirect'].should.equal '/foo/bar/hello.txt'
+    end
+  end
+
+  specify 'writes to rack.error when no X-Accel-Mapping is specified' do
+    request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response|
+      response.should.be.ok
+      response.body.should.equal 'Hello World'
+      response.headers.should.not.include 'X-Accel-Redirect'
+      response.errors.should.include 'X-Accel-Mapping'
+    end
+  end
+
+  specify 'does nothing when body does not respond to #to_path' do
+    @request = Rack::MockRequest.new(sendfile_app(['Not a file...']))
+    request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+      response.body.should.equal 'Not a file...'
+      response.headers.should.not.include 'X-Sendfile'
+    end
+  end
+end