#!/usr/local/bin/ruby
##
# @file mpmws.rb
# @author Mitch Richling <http://www.mitchr.me/>
# @Copyright Copyright 2006 by Mitch Richling. All rights reserved.
# @Revision $Revision: 1.11 $
# @SCMdate $Date: 2011/06/23 19:46:05 $
# @brief Simple web server with Ruby.@EOL
# @Keywords ruby web server webrick
# @Std Ruby 1.8
#
# INTRODUCTION
#
# This little ruby program implements a simple web server providing browsable directory index
# lists, HTML index support (index.html), and CGI support. While not as sophisticated as web
# servers like Apache, this web server is more than adequate for testing things like CGI scripts
# client side AJAX-style code.
#
# HTML index files are supported, and will be delivered if the URL points to a directory containing
# an appropriately named index file ("index.html", "index.htm", "index.cgi", or
# "index.rhtml). Apache-like directory indexes are generated for directories not containing an HTML
# index file. Files ending with .cgi will be executed as CGI scripts. A small, but normalcy
# sufficient Mime-Type map is provided as well. The IP address to bind to, the port to listen on,
# and the directory for the web server document root may be provided as command line arguments.
#
# IMPLEMENTATION HISTORY
#
# This script was born from a need to test different web sites on one stand alone laptop in such
# a way that each site was isolated from the rest on a different web sever. Testing CGI scripts
# and providing an endpoint for XML queries in client side AJAX code drove the need for a
# server, and eliminated the possibility of testing with a static filesystem. This script provides
# the HTTP server required without the complexity associated with normal HTTP servers.
#
# USAGE
#
# USE: mpmws.rb [OPTIONS] [[ADDRESS:]PORT] [DIRECTORY]
#
# First options (arguments begining with '-') are poped off and processed:
# -s -- Turn on SSL if ruby >= 1.8.6. (Note: use https in the URL)
# -p=file -- Specify a password file. Turns on -a as well. If you leave off the name
# and equals sign, then ~/.mpmpw.txt is used.
# Each line of the file looks like "user_name:SHA1_hash_of_passwd" or
# "user_name:clear_password". Clear passwords are recognized as not being 40
# hex digits. :) One can generate a simple file (user: mitch, passwd: mitch1)
# like this:
# echo -n 'mitch:' > pw.txt; echo 'mitch1' | openssl sha1 >> pw.txt
# -up=u:p -- Specify a user name and password on the command line. Turns on -a as well.
# This option may be present multiple times -- each time a new user &
# password will be added to the list, or will update password for an existing
# user. The -p & -up options are processed in the order they appear on the
# command line. The value given to this option is processed just like a
# single line of a password file given to -p
#
# If only one argument remains and it is an existing directory, then it is assumed to be the
# DIRECTORY argument. Otherwise a single argument will be interpreted as the
# [[ADDRESS:]PORT] argument. If no arguments are given, or
# some are missing, then the defaults are used. The ADDRESS defaults to 'localhost', the
# PORT defaults to '8080', and the DIRECTORY defaults to the current working directory ('./').
#
# With the defaults, most UNIX browsers will attache to the web server with
# "http://localhost:8080/". Some dumb browsers try to do direct DNS lookups on hosts in URLs
# and will need this URL: "http://127.0.0.1:8080/". Note, if the host is not configured
# properly, then 'localhost' may not resolve to '127.0.0.1'.
#
# EXIT CODES
#
# * 1 Too many command line arguments given!
# * 2 Given path doesn't exist!
# * 3 Given path exists, but is not a directory!
# * 4 Given path is a directory, but is not a executable!
# * 5 Given path is a directory, but is not a readable!
# * 6 Only root (uid=0) may open ports below 1024!
# * 7 The password file was bad
#
# TODO (not prioritized)
#
# * Add the ability to have multiple, different directories served up
# * Add command line options to add Mime-Types (point to a mimetype file for example)
# * Add command line options to change the HTML index file names
# * Add command line options to create a real 'cgi-bin' directory in which all file are executed
# as CGI scripts not just the ones with a .cgi extension.
# * Add help command line option
# * Redo the argument processing.
#
# BUGS
#
# * This script is so simple that not many bugs can exist. Still, I'm sure we have some. :)
require 'webrick'
include WEBrick
require 'cgi'
require 'digest/sha1'
require 'etc'
if (RUBY_VERSION >= '1.8.6') then
require 'webrick/https'
end
############################################################################################################################################
# Get command line arguments
dir = './'
srvAndPort = nil
doSSL = false
osUsrAndPass = Array.new
while (ARGV[0] =~ /^-/) do
curOpt = ARGV.shift
if (/^-+s/i =~ curOpt) then
if (RUBY_VERSION >= '1.8.6') then
doSSL = true
else
STDOUT.puts("WARNING(mpmws): SSL (HTTPS) won't work with Ruby pre 1.8.6 -- ignoreing -s option")
end
elsif (tmp = curOpt.match(/^-+up=(.+)$/i)) then
upv = tmp[1]
if (RUBY_VERSION >= '1.8.6') then
if (tmp = upv.match(/^([^:]+):(.+)$/)) then
osUsrAndPass.push( [ tmp[1], tmp[2] ] )
else
STDERR.puts("WARNING(mpmws): Value of -up was badly formatted: '#{upv}")
end
else
STDOUT.puts("WARNING(mpmws): HTTPAuth won't work with Ruby pre 1.8.6 -- ignoreing -p option")
end
elsif (tmp = curOpt.match(/^-+p(=(.+)){0,1}$/i)) then
pwFile = tmp[2]
if (pwFile.nil?) then
if (ENV['HOME']) then
pwFile = ENV['HOME'] + '/.mpmpw.txt'
else
pwFile = Etc.getpwnam(Etc.getlogin).dir + '/.mpmpw.txt'
end
end
if (RUBY_VERSION >= '1.8.6') then
open(pwFile) do |file|
file.each_line do |line|
if (tmp = line.strip.match(/^([^:]+):(.+)$/)) then
osUsrAndPass.push( [ tmp[1], tmp[2] ] )
else
STDERR.puts("WARNING(mpmws): Password file had a bad line: '#{line}")
end
end
end
else
STDOUT.puts("WARNING(mpmws): HTTPAuth won't work with Ruby pre 1.8.6 -- ignoreing -p option")
end
end
end
if (ARGV.size == 0) then
# Nothing to do...
elsif (ARGV.size == 1) then
if ( FileTest.directory?(ARGV[0])) then
dir = ARGV[0]
else
srvAndPort = ARGV[0]
end
elsif (ARGV.size == 2) then
srvAndPort = ARGV[0]
dir = ARGV[1]
else
puts("ERROR(mpmws): Too many command line arguments given!")
exit(1)
end
############################################################################################################################################
# Figure out the bindAdd and bindPort
bindAdd = 'localhost'
bindPort = 8080
if ( !(srvAndPort)) then
# nothing do do
elsif (/^\d+$/.match(srvAndPort)) then
bindPort = srvAndPort.to_i
elsif(/^.+:\d+$/.match(srvAndPort)) then
(bindAdd, bindPort) = srvAndPort.split(/:/)
bindPort = bindPort.to_i
else
bindAdd = srvAndPort
end
if ((bindAdd == 'localhost') || (bindAdd == 'loopback')) then
bindAdd = '127.0.0.1'
end
if (bindAdd == 'external') then
bindAdd = Socket.gethostbyname(Socket.gethostname)[0]
end
if (bindAdd == '*') then
bindAdd = nil
end
############################################################################################################################################
# Print out what we think we were ask to do
STDOUT.puts("INFO(mpmws): Requested Web Server Information:\n")
STDOUT.puts("INFO(mpmws): Bind Address: #{bindAdd}\n")
STDOUT.puts("INFO(mpmws): Bind Port: #{bindPort}\n")
STDOUT.puts("INFO(mpmws): Server Root: #{dir}\n")
############################################################################################################################################
# Make sure we can read the directory
if ( !(FileTest.exist?(dir))) then
STDERR.puts("ERROR(mpmws): Given path (#{dir}) doesn't exist!")
exit(2)
end
if ( !(FileTest.directory?(dir))) then
STDERR.puts("ERROR(mpmws): Given path (#{dir}) exists, but is not a directory!")
exit(3)
end
if ( !(FileTest.executable?(dir))) then
STDERR.puts("ERROR(mpmws): Given path (#{dir}) is a directory, but is not a executable!")
exit(4);
end
if ( !(FileTest.readable?(dir))) then
STDERR.puts("ERROR(mpmws): Given path (#{dir}) is a directory, but is not a readable!")
exit(5);
end
############################################################################################################################################
# Make sure port number is OK
if ( (bindPort < 1024) && (Process.uid != 0) ) then
STDERR.puts("ERROR(mpmws): Only root (uid=0) may open ports below 1024!")
exit(6);
end
############################################################################################################################################
# How we authenticate
thePasswords = Hash.new
if (!(osUsrAndPass.empty?)) then
osUsrAndPass.each do |u, p|
if (p =~ /^([a-z0-9]{40}|[A-Z0-9]{40})$/) then
thePasswords[tmp[1]] = tmp[2]
else
thePasswords[tmp[1]] = Digest::SHA1.hexdigest(tmp[2])
STDERR.puts("WARNING(mpmws): Password didn't look like a SHA1 hash. Hashed it: '#{u}:HIDDEN'")
end
end
end
authenticate = Proc.new do |req, res|
HTTPAuth.basic_auth(req, res, '') do |user, password|
thePasswords.empty? || (thePasswords[user] == Digest::SHA1.hexdigest(password || ''))
end
end
############################################################################################################################################
# Create the server object, trap signals, and start server up.
STDOUT.puts("INFO(mpmws): Starting up web server now...")
aServer = nil
if (doSSL) then
aServer = HTTPServer.new(:Port => bindPort, :BindAddress => bindAdd,
:SSLEnable => true,
:SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
:SSLCertName => [ ["C","US"], ["O","127.0.0.1"], ["CN", "127.0.0.1"] ]
)
else
aServer = HTTPServer.new(:Port => bindPort, :BindAddress => bindAdd)
end
aServer.mount('/', HTTPServlet::FileHandler, dir, :FancyIndexing => true, :HandlerCallback => authenticate)
# Trap server shutdown signals
if (RUBY_PLATFORM =~ /mswin/) then
['INT' , 'TERM'].each do |sig|
trap(sig) { aServer.shutdown }
end
else
['HUP', 'QUIT', 'INT', 'TERM', 'USR1', 'USR2'].each do |sig|
trap(sig) { aServer.shutdown }
end
end
aServer.start
Generated by GNU Enscript 1.6.5.2.