#!/usr/bin/ruby -w # $Id: httpd,v 1.6 2000/11/08 20:48:23 hip Exp hip $ # # httpd daemon for local use, supports CGI # http://www.xs4all.nl/~hipster/lib/ruby/httpd # # Copyright (C) 2000 Michel van de Ven # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA require "socket" require "thread" REVISION = /\d+(\.\d+)*/.match("$Revision: 1.6 $")[0] # edit these to fit your local setup: # root of the html tree DOCROOT = File.expand_path("~/lib/html") # root of the cgi scripts, relative to DOCROOT CGIROOT = "/cgi" # location of the logfile LOGFILE = File.expand_path("~/lib/html/httpd.log") # number of simultaneous handler threads THREADS = 4 # -------- HTTPOK = "HTTP/1.0 200 OK\n" class MimeMap def initialize @map = Hash.new("*/*") File.open("/etc/mime.types").each{ |line| next if line =~ /^#|^\s*$/ type, extlist = line.chomp.split(/\t+/) if extlist ext = extlist.split(/ /) ext.each{ |i| @map[i] = type } end } end def typeof ext @map[ext] end end class Log def Log.open stream = LOGFILE begin @io = (stream.is_a? IO) ? stream : File.open(stream, "a") rescue $stderr.puts "httpd: cannot open logfile for writing; logging to stderr" @io = $stderr end end def Log.close @io.flush @io.close end def Log.puts txt @io.print txt.chomp + "\n" @io.flush end end class Request attr_reader :socket, :header, :method, :file, :mtype, :content def initialize socket, mimemap @socket = socket @header = @method = @file = @content = @mtype = "" contentlength = 0 while (line = @socket.gets) != nil break if line == "\r\n" or line == "\n" @header << line case line when /^(GET|POST) (.*) HTTP\/\d+\.\d+/ Log.puts "[" + @socket.peeraddr[3] + "] " + line @method = $1 @file = $2 when /^content-length: (\d+)/i contentlength = $1.to_i end end if contentlength != 0 @content = @socket.read contentlength end # default to index.html for root requests @file = "/index.html" if @file == "/" # get file type from the extension or default to text if @file =~ /\.([a-z]+)$/ then ext = $1 else ext = "txt" end # set appropriate mimetype @mtype = mimemap.typeof(ext) end end class Httpd # host = 0.0.0.0: serve other hosts than localhost too def initialize host = "localhost", port = 8080 Log.open @mimemap = MimeMap.new @socket = TCPserver.new(host, port) @rqueue = Queue.new # request queue, handler threads query this @hostname = host @port = port @nrequest = 0 @cgi_mutex = Mutex.new ["SIGTERM", "SIGHUP", "SIGINT"].each{ |sig| trap sig, lambda { shutdown } } end def listen Log.puts "-" * 79 + "\nstart @ " + Time.now.to_s + "\nhttpd/#{REVISION}; " + "pid #{$$} with #{THREADS} threads listening on #{@hostname}:#{@port}" # start handler threads THREADS.times{ Thread.new{ handler } } # main accept loop loop do begin @rqueue.push @socket.accept @nrequest += 1 rescue Log.puts "socket exception: " + $! end end end private def handler loop do socket = @rqueue.pop req = Request.new socket, @mimemap if req.file =~ /^#{CGIROOT}/ handle_cgi req else if test(?f, DOCROOT + req.file) handle_file req elsif test(?d, DOCROOT + req.file) handle_dir req else handle_error req end end socket.close end end def handle_cgi req script, param = req.file.split("?") if test(?x, "#{DOCROOT}#{script}") begin @cgi_mutex.lock ENV["SERVER_SOFTWARE"] = "Ruby-httpd/#{REVISION}" ENV["REMOTE_HOST"] = ENV["HOSTNAME"] ENV["REQUEST_METHOD"] = req.method ENV["SCRIPT_NAME"] = script ENV["QUERY_STRING"] = param ENV["CONTENT_LENGTH"] = req.content.length != 0 ? req.content.length.to_s : nil cgi = IO.popen("#{DOCROOT}#{script}", "r+") if req.content.length != 0 cgi << req.content cgi.flush end result = cgi.read rescue result = "Content-type: text/html\n\n" + "

Error executing script '#{script}'

" ensure cgi.close @cgi_mutex.unlock end emit req.socket, HTTPOK + result else emit req.socket, HTTPOK + "Content-type: text/html\n\n" + "

Script '#{script}' not found, or not executable

\n" end end def handle_dir req emit req.socket, HTTPOK + "Content-type: text/html\n\n" + "

Directory listing of '#{req.file}'

\n" up = File.expand_path(req.file + "/..", DOCROOT) emit req.socket, %!up to higher level directory
\n! begin dir = Dir.open(DOCROOT + req.file) dir.sort.each{ |entry| next if entry == "." or entry == ".." link = entry link += "/" if test(?d, DOCROOT + req.file + "/" + link) emit req.socket, %!
#{link}
\n! } ensure dir.close end emit req.socket, "

" end def handle_file req content = File.open(DOCROOT + req.file).read emit req.socket, HTTPOK + "Content-type: #{req.mtype}\n\n" emit req.socket, content end def handle_error req if req.mtype =~ /^text\// emit req.socket, HTTPOK + "Content-type: text/html\n\n" + "

Object not found

\n" + "Request header:
#{req.header}
" else # nothing end end def emit socket, data begin socket.write data rescue Log.puts "socket exception: " + $! end end def shutdown Log.puts "#{@nrequest} requests handled" Log.puts "shutdown @ #{Time.now.to_s}" @socket.close Log.close exit end end if fork == nil Process.setsid # create new session, disconnect from tty [$stdin, $stdout, $stderr].each{ |stream| stream.reopen "/dev/null" } daemon = Httpd.new daemon.listen end