# #-- # Ronin Web - A Ruby library for Ronin that provides support for web # scraping and spidering functionality. # # Copyright (c) 2006-2009 Hal Brodigan (postmodern.mod3 at gmail.com) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #++ # require 'uri' require 'cgi' begin require 'mongrel' rescue require 'webrick' end require 'rack' module Ronin module Web class Server # Default interface to run the Web Server on HOST = '0.0.0.0' # Default port to run the Web Server on PORT = 8080 # Directory index files INDICES = ['index.htm', 'index.html'] # The host to bind to attr_accessor :host # The port to listen on attr_accessor :port # The Hash of configurable variables for the server attr_reader :config # # Creates a new Web Server using the given configuration _block_. # # _options_ may contain the following keys: # :host:: The host to bind to. # :port:: The port to listen on. # :config:: A +Hash+ of configurable variables to be used # in responses. # def initialize(options={},&block) @host = options[:host] @port = options[:port] @config = {} if options.has_key?(:config) @config.merge!(options[:config]) end @default = method(:not_found) @virtual_host_patterns = {} @virtual_hosts = {} @path_patterns = {} @paths = {} @directories = {} instance_eval(&block) if block end # # Returns the default host that the Web Server will be run on. # def Server.default_host @@default_host ||= HOST end # # Sets the default host that the Web Server will run on to the # specified _host_. # def Server.default_host=(host) @@default_host = host end # # Returns the default port that the Web Server will run on. # def Server.default_port @@default_port ||= PORT end # # Sets the default port the Web Server will run on to the specified # _port_. # def Server.default_port=(port) @@default_port = port end # # The Hash of the servers supported file extensions and their HTTP # Content-Types. # def Server.content_types @@content_types ||= {} end # # Registers a new content _type_ for the specified file _extensions_. # # Server.content_type 'text/xml', ['xml', 'xsl'] # def self.content_type(type,extensions) extensions.each { |ext| Server.content_types[ext] = type } return self end # # Runs the specified _server_ with the given _options_. Server.run # will use Mongrel to run the _server_, if it is installed. Otherwise # WEBrick will be used to run the _server_. # # _options_ can contain the following keys: # :host:: The host the server will bind to, defaults to # Server.default_host. # :port:: The port the server will listen on, defaults to # Server.default_port. # def Server.run(server,options={}) rack_options = {} rack_options[:Host] = (options[:host] || Server.default_host) rack_options[:Port] = (options[:port] || Server.default_port) if Object.const_defined?('Mongrel') Rack::Handler::Mongrel.run(server,rack_options) else Rack::Handler::WEBrick.run(server,rack_options) end end # # Creates a new Web Server object with the given _block_ and starts # it using the given _options_. # def self.start(options={},&block) self.new(options,&block).start end # # Returns the HTTP Content-Type for the specified file _extension_. # # content_type('html') # # => "text/html" # def content_type(extension) Server.content_types[extension] || 'application/x-unknown-content-type' end # # Returns the HTTP Content-Type for the specified _file_. # # srv.content_type_for('file.html') # # => "text/html" # def content_type_for(file) ext = File.extname(file).downcase return content_type(ext[1..-1]) end # # Returns the index file contained within the _path_ of the specified # directory. If no index file can be found, +nil+ will be returned. # def index_of(path) path = File.expand_path(path) INDICES.each do |name| index = File.join(path,name) return index if File.file?(index) end return nil end # # Returns the HTTP 404 Not Found message for the requested path. # def not_found(env) path = env['PATH_INFO'] body = %{ 404 Not Found

Not Found

The requested URL #{CGI.escapeHTML(path)} was not found on this server.


} return response(body, :status => 404, :content_type => 'text/html') end # # Returns the contents of the file at the specified _path_. If the # _path_ points to a directory, the directory will be searched for # an index file. If no index file can be found or _path_ points to a # non-existant file, a "404 Not Found" response will be returned. # def return_file(path,env) if !(File.exists?(path)) return not_found(env) end if File.directory?(path) unless (path = index_of(path)) return not_found(env) end end return response(File.new(path), :content_type => content_type_for(path)) end # # Returns a Rack Response object with the specified _body_, the given # _options_ and the given _block_. # # _options_ may include the following keys: # :status:: The HTTP Response status code, defaults to 200. # # response("lol", :content_type => 'text/xml') # def response(body=[],options={},&block) status = (options.delete(:status) || 200) headers = {} options.each do |name,value| header_name = name.to_s.split('_').map { |word| word.capitalize }.join('-') headers[header_name] = value.to_s end return Rack::Response.new(body,status,headers,&block) end # # Use the specified _block_ as the default route for all other # requests. # # default do |env| # [200, {'Content-Type' => 'text/html'}, 'lol train'] # end # def default(&block) @default = block return self end # # Connects the specified _server_ as a virtual host representing the # specified host _name_. # def connect(name,server) @virtual_hosts[name.to_s] = server end # # Returns the server that handles requests for the specified host # _name_. # def virtual_host(name) name = name.to_s if @virtual_hosts.has_key?(name) return @virtual_hosts[name] end @virtual_host_patterns.each do |pattern,server| return server if name.match(pattern) end return nil end # # Registers the specified _block_ to be called when receiving # requests to host names which match the specified _pattern_. # # hosts_like(/^a[0-9]\./) do # map('/download/') do |env| # ... # end # end # def hosts_like(pattern,&block) @virtual_host_patterns[pattern] = self.class.new(&block) end # # Registers the specified _block_ to be called when receiving # requests for paths which match the specified _pattern_. # # paths_like(/\.xml$/) do |env| # ... # end # def paths_like(pattern,&block) @path_patterns[pattern] = block return self end # # Creates a new Server object using the specified _block_ and # connects it as a virtual host representing the specified host # _name_. # # host('cdn.evil.com') do # ... # end # def host(name,&block) connect(name,self.class.new(&block)) end # # Binds the specified URL _path_ to the given _block_. # # bind '/secrets.xml' do |env| # [200, {'Content-Type' => 'text/xml'}, "Made you look."] # end # def bind(path,&block) @paths[path] = block return self end # # Binds the specified URL directory _path_ to the given _block_. # # map '/downloads' do |env| # response( # "Your somewhere inside the downloads directory", # :content_type' => 'text/xml' # ) # end # def map(path,&block) @directories[path] = block return self end # # Binds the contents of the specified _file_ to the specified URL # _path_, using the given _options_. # # file '/robots.txt', '/path/to/my_robots.txt' # def file(path,file,options={}) file = File.expand_path(file) content_type = (options[:content_type] || content_type_for(file)) bind(path) do |env| if File.file?(file) return_file(file,env) else not_found(env) end end end # # Mounts the contents of the specified _directory_ to the given # prefix _path_. # # mount '/download/', '/tmp/files/' # def mount(path,directory) sub_dirs = path.split('/') directory = File.expand_path(directory) map(path) do |env| http_path = File.expand_path(env['PATH_INFO']) http_dirs = http_path.split('/') sub_path = http_dirs[sub_dirs.length..-1].join('/') absolute_path = File.join(directory,sub_path) return_file(absolute_path,env) end end # # Starts the server. # def start Server.run(self, :host => @host, :port => @port) return self end # # The method which receives all requests. # def call(env) http_host = env['HTTP_HOST'] http_path = File.expand_path(env['PATH_INFO']) if http_host if (server = virtual_host(http_host)) return server.call(env) end end if http_path if (block = @paths[http_path]) return block.call(env) end @path_patterns.each do |pattern,block| if http_path.match(pattern) return block.call(env) end end http_dirs = http_path.split('/') sub_dir = @directories.keys.select { |path| dirs = path.split('/') http_dirs[0...dirs.length] == dirs }.sort.last if (sub_dir && (block = @directories[sub_dir])) return block.call(env) end end return @default.call(env) end # # Routes the specified _url_ to the call method. # def route(url) url = URI(url.to_s) return call( 'HTTP_HOST' => url.host, 'HTTP_PORT' => url.port, 'SERVER_PORT' => url.port, 'PATH_INFO' => url.path, 'QUERY_STRING' => url.query ) end # # Routes the specified _path_ to the call method. # def route_path(path) path, query = URI.decode(path.to_s).split('?',2) return route(URI::HTTP.build( :host => @host, :port => @port, :path => path, :query => query )) end protected content_type 'text/html', ['html', 'htm', 'xhtml'] content_type 'text/css', ['css'] content_type 'text/gif', ['gif'] content_type 'text/jpeg', ['jpeg', 'jpg'] content_type 'text/png', ['png'] content_type 'image/x-icon', ['ico'] content_type 'text/javascript', ['js'] content_type 'text/xml', ['xml', 'xsl'] content_type 'application/rss+xml', ['rss'] content_type 'application/rdf+xml', ['rdf'] content_type 'application/pdf', ['pdf'] content_type 'application/doc', ['doc'] content_type 'application/zip', ['zip'] content_type 'text/plain', ['txt', 'conf', 'rb', 'py', 'h', 'c', 'hh', 'cc', 'hpp', 'cpp'] end end end