1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/local/bin/ruby

##
# @file      mpmws.rb
# @author    Mitch Richling <https://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/) || (RUBY_PLATFORM =~ /mingw/) || (RUBY_PLATFORM =~ /cygwin/)) 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