# HG changeset patch # User Mahlon E. Smith # Date 1189006275 0 # Wed Sep 05 15:31:15 2007 +0000 # Branch content_negotiation # Node ID 4ca66fe7abdf267cfa10da448991ecb8f1c33d6e # Parent c2ca48baeeefdda9769ab3b4e533300aaf727aa9 * Coverage added for StaticContentHandler. * Add an accessor to StaticContentHandler -- possible use: dynamic static resource directory selection based on arbitrary conditionals!!! YEAH. A new para-dig-im! diff --git a/lib/thingfish/exceptions.rb b/lib/thingfish/exceptions.rb --- a/lib/thingfish/exceptions.rb +++ b/lib/thingfish/exceptions.rb @@ -43,7 +43,9 @@ class FileStoreQuotaError < ThingFish::FileStoreError; end # Something was wrong with a request - class RequestError < ThingFish::Error; end + class RequestError < ThingFish::Error + + end # Something was wrong with a response class ResponseError < ThingFish::Error; end diff --git a/lib/thingfish/handler/staticcontent.rb b/lib/thingfish/handler/staticcontent.rb --- a/lib/thingfish/handler/staticcontent.rb +++ b/lib/thingfish/handler/staticcontent.rb @@ -57,11 +57,16 @@ def initialize( basedir ) @basedir = basedir.expand_path end - + - ######### - protected - ######### + ###### + public + ###### + + # The directory this handler should use as its base + # :TODO: Document use-case of changing this on the fly based on Host: header. + attr_accessor :basedir + ### Handle a GET request def handle_get_request( request, response ) @@ -129,6 +134,10 @@ end + ######### + protected + ######### + ### Given a +path+, try to map it to a file under the handler's base directory. ### Return a Pathname object to the requested file if valid, +nil+ if not. def get_safe_path( path ) diff --git a/spec/staticcontent_spec.rb b/spec/staticcontent_spec.rb --- a/spec/staticcontent_spec.rb +++ b/spec/staticcontent_spec.rb @@ -37,8 +37,8 @@ describe ThingFish::StaticContentHandler do before( :each ) do - resdir = Pathname.new( __FILE__ ).expand_path.dirname.parent + 'var/www' - @handler = ThingFish::Handler.create( 'static', 'resource_dir' => resdir ) + resdir = Pathname.new( __FILE__ ).expand_path.dirname.parent + '/tmp' + @handler = ThingFish::Handler.create( 'staticcontent', resdir ) @listener = mock( "thingfish daemon", :null_object => true ) @handler.listener = @listener @@ -50,12 +50,129 @@ @request.stub!( :headers ).and_return( @request_headers ) @response.stub!( :headers ).and_return( @response_headers ) + + @pathname = mock( "Pathname", :null_object => true ) end ### Shared behaviors it_should_behave_like "A Handler" + it "does nothing if the requested path doesn't exist" do + @handler.stub!( :get_safe_path ).and_return( @pathname ) + @pathname.should_receive( :exist? ).and_return( false ) + + @response.should_not_receive( :status= ) + @handler.handle_get_request( @request, @response ) + end + + + it "refuses to serve directories" do + @handler.stub!( :get_safe_path ).and_return( @pathname ) + @pathname.should_receive( :exist? ).and_return( true ) + @pathname.should_receive( :file? ).and_return( false ) + + @response.should_receive( :status= ).with( HTTP::FORBIDDEN ) + @handler.handle_get_request( @request, @response ) + end + + + it "refuses to serve files outside of its resource directory" do + @request.should_receive( :path_info ). + and_return( '../../etc/shadow' ) + + @response.should_not_receive( :status= ) + @handler.handle_get_request( @request, @response ) + end + + + it "respects file access permissions" do + @handler.stub!( :get_safe_path ).and_return( @pathname ) + @pathname.should_receive( :exist? ).and_return( true ) + @pathname.should_receive( :file? ).and_return( true ) + @pathname.should_receive( :readable? ).and_return( false ) + + @response.should_receive( :status= ).with( HTTP::FORBIDDEN ) + @handler.handle_get_request( @request, @response ) + end + + + it "sets the appropriate content type for the file" do + @handler.stub!( :get_safe_path ).and_return( @pathname ) + + @pathname.should_receive( :extname ).and_return( '.html' ) + @response_headers. + should_receive( :[]= ). + with( :content_type, 'text/html' ) + + @pathname.should_receive( :size ).and_return( 7 ) + @response_headers. + should_receive( :[]= ). + with( :content_length, 7 ) + + @response.should_receive( :status= ).with( HTTP::OK ) + + @pathname.should_receive( :open ).with('r').and_return( :an_IO ) + @response.should_receive( :body= ).with( :an_IO ) + + @handler.handle_get_request( @request, @response ) + end + + + it "defaults to a generic content type as fallback" do + @handler.stub!( :get_safe_path ).and_return( @pathname ) + + @pathname.should_receive( :extname ).and_return( '.floop' ) + @response_headers. + should_receive( :[]= ). + with( :content_type, ThingFish::StaticContentHandler::DEFAULT_CONTENT_TYPE ) + + @pathname.should_receive( :size ).and_return( 7938844 ) + @response_headers. + should_receive( :[]= ). + with( :content_length, 7938844 ) + + @response.should_receive( :status= ).with( HTTP::OK ) + + @pathname.should_receive( :open ).with('r').and_return( :an_IO ) + @response.should_receive( :body= ).with( :an_IO ) + + @handler.handle_get_request( @request, @response ) + end + + + INDEX_FILE = ThingFish::StaticContentHandler::DEFAULT_INDEX_FILE + + it "serves index files" do + @request.should_receive( :path_info ). + and_return( 'this_is_a_directory_with_an_index_file' ) + @handler.stub!( :get_safe_path ).and_return( @pathname ) + + @pathname.should_receive( :directory? ).and_return( true ) + @indexpath = mock( "index file Pathname object" ) + @pathname.should_receive( :+ ).with( INDEX_FILE ).and_return( @indexpath ) + + @indexpath.should_receive( :exist? ).and_return( true ) + @indexpath.should_receive( :file? ).and_return( true ) + @indexpath.should_receive( :readable? ).and_return( true ) + + @indexpath.should_receive( :extname ).and_return( '.floop' ) + @response_headers. + should_receive( :[]= ). + with( :content_type, ThingFish::StaticContentHandler::DEFAULT_CONTENT_TYPE ) + + @indexpath.should_receive( :size ).and_return( 7938844 ) + @response_headers. + should_receive( :[]= ). + with( :content_length, 7938844 ) + + @response.should_receive( :status= ).with( HTTP::OK ) + + @indexpath.should_receive( :open ).with('r').and_return( :an_IO ) + @response.should_receive( :body= ).with( :an_IO ) + + @handler.handle_get_request( @request, @response ) + end end