55 changes: 46 additions & 9 deletions lib/webrick/httprequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@
require 'webrick/cookie'

module WEBrick

##
# An HTTP request.
class HTTPRequest

BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ]

# Request line
# :section: Request line
attr_reader :request_line
attr_reader :request_method, :unparsed_uri, :http_version

# Request-URI
# :section: Request-URI
attr_reader :request_uri, :path
attr_accessor :script_name, :path_info, :query_string

# Header and entity body
# :section: Header and entity body
attr_reader :raw_header, :header, :cookies
attr_reader :accept, :accept_charset
attr_reader :accept_encoding, :accept_language

# Misc
# :section:
attr_accessor :user
attr_reader :addr, :peeraddr
attr_reader :attributes
Expand Down Expand Up @@ -137,60 +141,93 @@ def body(&block)
@body.empty? ? nil : @body
end

##
# Request query as a Hash

def query
unless @query
parse_query()
end
@query
end

##
# The content-length header

def content_length
return Integer(self['content-length'])
end

##
# The content-type header

def content_type
return self['content-type']
end

##
# Retrieves +header_name+

def [](header_name)
if @header
value = @header[header_name.downcase]
value.empty? ? nil : value.join(", ")
end
end

##
# Iterates over the request headers

def each
@header.each{|k, v|
value = @header[k]
yield(k, value.empty? ? nil : value.join(", "))
}
end

##
# The host this request is for

def host
return @forwarded_host || @host
end

##
# The port this request is for

def port
return @forwarded_port || @port
end

##
# The server name this request is for

def server_name
return @forwarded_server || @config[:ServerName]
end

##
# The client's IP address

def remote_ip
return self["client-ip"] || @forwarded_for || @peeraddr[3]
end

##
# Is this an SSL request?

def ssl?
return @request_uri.scheme == "https"
end

##
# Should the connection this request was made on be kept alive?

def keep_alive?
@keep_alive
end

def to_s
def to_s # :nodoc:
ret = @request_line.dup
@raw_header.each{|line| ret << line }
ret << CRLF
Expand All @@ -210,11 +247,11 @@ def fixup()
end
end

def meta_vars
# This method provides the metavariables defined by the revision 3
# of ``The WWW Common Gateway Interface Version 1.1''.
# (http://Web.Golux.Com/coar/cgi/)
# This method provides the metavariables defined by the revision 3
# of "The WWW Common Gateway Interface Version 1.1"
# http://Web.Golux.Com/coar/cgi/

def meta_vars
meta = Hash.new

cl = self["Content-Length"]
Expand Down
72 changes: 70 additions & 2 deletions lib/webrick/httpresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@
require 'webrick/httpstatus'

module WEBrick
##
# An HTTP response.

class HTTPResponse
attr_reader :http_version, :status, :header
attr_reader :cookies
attr_accessor :reason_phrase

##
# Body may be a String or IO subclass.

attr_accessor :body

attr_accessor :request_method, :request_uri, :request_http_version
attr_accessor :filename
attr_accessor :keep_alive
attr_reader :config, :sent_size

##
# Creates a new HTTP response object

def initialize(config)
@config = config
@buffer_size = config[:OutputBufferSize]
Expand All @@ -45,57 +55,96 @@ def initialize(config)
@sent_size = 0
end

##
# The response's HTTP status line

def status_line
"HTTP/#@http_version #@status #@reason_phrase #{CRLF}"
end

##
# Sets the response's status to the +status+ code

def status=(status)
@status = status
@reason_phrase = HTTPStatus::reason_phrase(status)
end

##
# Retrieves the response header +field+

def [](field)
@header[field.downcase]
end

##
# Sets the response header +field+ to +value+

def []=(field, value)
@header[field.downcase] = value.to_s
end

##
# The content-length header

def content_length
if len = self['content-length']
return Integer(len)
end
end

##
# Sets the content-length header to +len+

def content_length=(len)
self['content-length'] = len.to_s
end

##
# The content-type header

def content_type
self['content-type']
end

##
# Sets the content-type header to +type+

def content_type=(type)
self['content-type'] = type
end

##
# Iterates over each header in the resopnse

def each
@header.each{|k, v| yield(k, v) }
@header.each{|field, value| yield(field, value) }
end

##
# Will this response body be returned using chunked transfer-encoding?

def chunked?
@chunked
end

##
# Enables chunked transfer encoding.

def chunked=(val)
@chunked = val ? true : false
end

##
# Will this response's connection be kept alive?

def keep_alive?
@keep_alive
end

##
# Sends the response on +socket+

def send_response(socket)
begin
setup_header()
Expand All @@ -110,6 +159,9 @@ def send_response(socket)
end
end

##
# Sets up the headers for sending

def setup_header()
@reason_phrase ||= HTTPStatus::reason_phrase(@status)
@header['server'] ||= @config[:ServerSoftware]
Expand Down Expand Up @@ -165,6 +217,9 @@ def setup_header()
end
end

##
# Sends the headers on +socket+

def send_header(socket)
if @http_version.major > 0
data = status_line()
Expand All @@ -180,25 +235,38 @@ def send_header(socket)
end
end

##
# Sends the body on +socket+

def send_body(socket)
case @body
when IO then send_body_io(socket)
else send_body_string(socket)
end
end

def to_s
def to_s # :nodoc:
ret = ""
send_response(ret)
ret
end

##
# Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+.
#
# Example:
#
# res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect

def set_redirect(status, url)
@body = "<HTML><A HREF=\"#{url.to_s}\">#{url.to_s}</A>.</HTML>\n"
@header['location'] = url.to_s
raise status
end

##
# Creates an error page for exception +ex+ with an optional +backtrace+

def set_error(ex, backtrace=false)
case ex
when HTTPStatus::Status
Expand Down
47 changes: 47 additions & 0 deletions lib/webrick/httpserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,28 @@
module WEBrick
class HTTPServerError < ServerError; end

##
# An HTTP Server

class HTTPServer < ::WEBrick::GenericServer
##
# Creates a new HTTP server according to +config+
#
# An HTTP server uses the following attributes:
#
# :AccessLog:: An array of access logs. See WEBrick::AccessLog
# :BindAddress:: Local address for the server to bind to
# :DocumentRoot:: Root path to serve files from
# :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler
# :HTTPVersion:: The HTTP version of this server
# :Port:: Port to listen on
# :RequestCallback:: Called with a request and response before each
# request is serviced.
# :RequestTimeout:: Maximum time to wait between requests
# :ServerAlias:: Array of alternate names for this server for virtual
# hosting
# :ServerName:: Name for this server for virtual hosting

def initialize(config={}, default=Config::HTTP)
super(config, default)
@http_version = HTTPVersion::convert(@config[:HTTPVersion])
Expand All @@ -40,6 +61,9 @@ def initialize(config={}, default=Config::HTTP)
@virtual_hosts = Array.new
end

##
# Processes requests on +sock+

def run(sock)
while true
res = HTTPResponse.new(@config)
Expand Down Expand Up @@ -93,6 +117,9 @@ def run(sock)
end
end

##
# Services +req+ and fills in +res+

def service(req, res)
if req.unparsed_uri == "*"
if req.request_method == "OPTIONS"
Expand All @@ -115,23 +142,37 @@ def do_OPTIONS(req, res)
res["allow"] = "GET,HEAD,POST,OPTIONS"
end

##
# Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation
# time

def mount(dir, servlet, *options)
@logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
@mount_tab[dir] = [ servlet, options ]
end

##
# Mounts +proc+ or +block+ on +dir+ and calls it with a
# WEBrick::HTTPRequest and WEBrick::HTTPResponse

def mount_proc(dir, proc=nil, &block)
proc ||= block
raise HTTPServerError, "must pass a proc or block" unless proc
mount(dir, HTTPServlet::ProcHandler.new(proc))
end

##
# Unmounts +dir+

def unmount(dir)
@logger.debug(sprintf("unmount %s.", dir))
@mount_tab.delete(dir)
end
alias umount unmount

##
# Finds a servlet for +path+

def search_servlet(path)
script_name, path_info = @mount_tab.scan(path)
servlet, options = @mount_tab[script_name]
Expand All @@ -140,6 +181,9 @@ def search_servlet(path)
end
end

##
# Adds +server+ as a virtual host.

def virtual_host(server)
@virtual_hosts << server
@virtual_hosts = @virtual_hosts.sort_by{|s|
Expand All @@ -151,6 +195,9 @@ def virtual_host(server)
}
end

##
# Finds the appropriate virtual host to handle +req+

def lookup_server(req)
@virtual_hosts.find{|s|
(s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) &&
Expand Down
87 changes: 85 additions & 2 deletions lib/webrick/httpservlet/abstract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,88 @@ module WEBrick
module HTTPServlet
class HTTPServletError < StandardError; end

##
# AbstractServlet allows HTTP server modules to be reused across multiple
# servers and allows encapsulation of functionality.
#
# By default a servlet will respond to GET, HEAD (through an alias to GET)
# and OPTIONS requests.
#
# By default a new servlet is initialized for every request. A servlet
# instance can be reused by overriding ::get_instance in the
# AbstractServlet subclass.
#
# == A Simple Servlet
#
# class Simple < WEBrick::HTTPServlet::AbstractServlet
# def do_GET request, response
# status, content_type, body = do_stuff_with request
#
# response.status = status
# response['Content-Type'] = content_type
# response.body = body
# end
#
# def do_stuff_with request
# return 200, 'text/plain', 'you got a page'
# end
# end
#
# This servlet can be mounted on a server at a given path:
#
# server.mount '/simple', Simple
#
# == Servlet Configuration
#
# Servlets can be configured via initialize. The first argument is the
# HTTP server the servlet is being initialized for.
#
# class Configureable < Simple
# def initialize server, color, size
# super server
# @color = color
# @size = size
# end
#
# def do_stuff_with request
# content = "<p " \
# %q{style="color: #{@color}; font-size: #{@size}"} \
# ">Hello, World!"
#
# return 200, "text/html", content
# end
# end
#
# This servlet must be provided two arguments at mount time:
#
# server.mount '/configurable', Configurable, 'red', '2em'

class AbstractServlet
def self.get_instance(config, *options)
self.new(config, *options)

##
# Factory for servlet instances that will handle a request from +server+
# using +options+ from the mount point. By default a new servlet
# instance is created for every call.

def self.get_instance(server, *options)
self.new(server, *options)
end

##
# Initializes a new servlet for +server+ using +options+ which are
# stored as-is in +@options+. +@logger+ is also provided.

def initialize(server, *options)
@server = @config = server
@logger = @server[:Logger]
@options = options
end

##
# Dispatches to a +do_+ method based on +req+ if such a method is
# available. (+do_GET+ for a GET request). Raises a MethodNotAllowed
# exception if the method is not implemented.

def service(req, res)
method_name = "do_" + req.request_method.gsub(/-/, "_")
if respond_to?(method_name)
Expand All @@ -39,14 +110,23 @@ def service(req, res)
end
end

##
# Raises a NotFound exception

def do_GET(req, res)
raise HTTPStatus::NotFound, "not found."
end

##
# Dispatches to do_GET

def do_HEAD(req, res)
do_GET(req, res)
end

##
# Returns the allowed HTTP request methods

def do_OPTIONS(req, res)
m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1}
m.sort!
Expand All @@ -55,6 +135,9 @@ def do_OPTIONS(req, res)

private

##
# Redirects to a path ending in /

def redirect_to_directory_uri(req, res)
if req.path[-1] != ?/
location = WEBrick::HTTPUtils.escape_path(req.path + "/")
Expand Down
31 changes: 31 additions & 0 deletions lib/webrick/httpservlet/filehandler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,48 @@ def prepare_range(range, filesize)
end
end

##
# Serves files from a directory

class FileHandler < AbstractServlet
HandlerTable = Hash.new

##
# Allow custom handling of requests for files with +suffix+ by class
# +handler+

def self.add_handler(suffix, handler)
HandlerTable[suffix] = handler
end

##
# Remove custom handling of requests for files with +suffix+

def self.remove_handler(suffix)
HandlerTable.delete(suffix)
end

##
# Creates a FileHandler servlet on +server+ that serves files starting
# at directory +root+
#
# If +options+ is a Hash the following keys are allowed:
#
# :AcceptableLanguages:: Array of languages allowed for accept-language
# :DirectoryCallback:: Allows preprocessing of directory requests
# :FancyIndexing:: If true, show an index for directories
# :FileCallback:: Allows preprocessing of file requests
# :HandlerCallback:: Allows preprocessing of requests
# :HandlerTable:: Maps file suffixes to file handlers.
# DefaultFileHandler is used by default but any servlet
# can be used.
# :NondisclosureName:: Do not show files matching this array of globs
# :UserDir:: Directory inside ~user to serve content from for /~user
# requests. Only works if mounted on /
#
# If +options+ is true or false then +:FancyIndexing+ is enabled or
# disabled respectively.

def initialize(server, root, options={}, default=Config::FileHandler)
@config = server.config
@logger = @config[:Logger]
Expand Down
14 changes: 14 additions & 0 deletions lib/webrick/log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,23 @@
# $IPR: log.rb,v 1.26 2002/10/06 17:06:10 gotoyuzo Exp $

module WEBrick

##
# A generic logging class

class BasicLog
# log-level constant
FATAL, ERROR, WARN, INFO, DEBUG = 1, 2, 3, 4, 5

attr_accessor :level

##
# Initializes a new logger for +log_file+ that outputs messages at +level+
# or higher. +log_file+ can be a filename, an IO-like object that
# responds to #<< or nil which outputs to $stderr.
#
# If no level is given INFO is chosen by default

def initialize(log_file=nil, level=nil)
@level = level || INFO
case log_file
Expand Down Expand Up @@ -71,6 +82,9 @@ def format(arg)
end
end

##
# A logging class with timestamps

class Log < BasicLog
attr_accessor :time_format

Expand Down
8 changes: 8 additions & 0 deletions lib/webrick/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ def SimpleServer.start
end
end

##
# A generic module for daemonizing a process

class Daemon

##
# Performs the standard operations for daemonizing a process. Runs a
# block, if given.

def Daemon.start
exit!(0) if fork
Process::setsid
Expand Down