DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

130 Lines Web Server

07.27.2010
| 2745 views |
  • submit to reddit
        Here a very tiny web server 

Warning! stop_browser() must be complete...

require 'socket'
require 'thread'

#################### Tiny embeded webserver
class Webserver
  def info(txt) ; puts "nw>i> #{txt}" ; end
  def error(txt) ; puts "nw>e> #{txt}" ; end
  def unescape(string) ; string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2}))/n) { [$1.delete('%')].pack('H*') } ;  end

  def initialize(port=7080,cadence=10,timeout=120)
	@last_mtime=File.mtime(__FILE__)
	@port=port
	@timeout=timeout
	@th={}
	@cb={}
	@redirect={}
	info("serveur http #{port} ...")
	@server = TCPServer.new('0.0.0.0', @port)
	@server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
	info("serveur http #{port} ready!")
	observe(cadence,120)
	Thread.new {
	begin
	  while (session = @server.accept)
		Thread.new(session) do |sess|
		   @th[Thread.current]=Time.now
		   request(sess) 
		   @th.delete(Thread.current) 
		end
	  end
	rescue
	  error($!.to_s)
	end
	}
  end
  def serve(uri,&blk)
	@cb[uri] = blk
  end  
  def observe(sleeping,delta)
	Thread.new do loop do
	  sleep(sleeping) 
	  nowDelta=Time.now-delta
	  l=@th.select { |th,tm| (tm<nowDelta) }
	  l.each { |th,tm| info("killing thread") ; th.kill; @th.delete(th)  }
	end ; end
  end
  def request(session)
	  request = session.gets
	  uri = (request.split(/\s+/)+['','',''])[1]
	  #info uri
	  service,param,*bidon=(uri+"?").split(/\?/)
	  params=Hash[*(param.split(/#/)[0].split(/[=&]/))] rescue {}
	  params.each { |k,v| params[k]=unescape(v) }
	  uri=unescape(service)[1..-1].gsub(/\.\./,"")
	  userpass=nil
	  if (buri=uri.split(/@/)).size>1
		uri=buri[1..-1].join("@")
		userpass=buri[0].split(/:/)
	  end
	  do_service(session,request,uri,userpass,params)
  rescue
	error("Error Web get on #{request}: \n #{$!.to_s} \n #{$!.backtrace.join("\n     ")}" ) rescue nil
	session.write "HTTP/1.1 501/NOK\rContent-type: text/html\r\n\r\n<html><head><title>WS</title></head><body>Error : #{$!}" rescue nil
  ensure
	session.close rescue nil
  end  
  def redirect(o,d)
   @redirect[o]=d
  end  
  def do_service(session,request,service,user_passwd,params)
	#info "request='"+service+"'"
	redir=@redirect["/"+service]
	service=redir.gsub(/^\//,"") if @redirect[redir]
	if redir &&  ! @redirect[redir] 
	  do_service(session,request,redir.gsub(/^\//,""),user_passwd,params)
	elsif @cb["/"+service]
	  begin
	   code,type,data= @cb["/"+service].call(params)
	   if code==0 && data != '/'+service
		  do_service(session,request,data[1..-1],user_passwd,params)
	   else
		 code==200 ?  sendData(session,type,data) : sendError(session,code,data)
	   end
	  rescue
	   puts "Error in get /#{service} : #{$!}"
	   sendError(session,501,$!.to_s)
	  end
	elsif service =~ /^stop/ 
	  sendData(session,".html","Stopping...");	   
	  Thread.new() { sleep(0.1); stop_browser()  }
	elsif File.directory?("./"+service)
	  sendData(session,".html",makeIndex("./"+service))
	elsif File.exists?(service)
	  sendFile(session,service)
	else
	  info("unknown request serv=#{service} params=#{params.inspect} #{File.exists?(service)}")
	  sendError(session,500);
	end
  end
  def stop_browser
	info "exit on web demand !"
	@serveur.close rescue nil
	exit!(0) 
  end
  def makeIndex(dir)
	dirs,files=Dir.glob(dir=="./" ? "*" :dir+"/*").sort.partition { |f| File.directory?(f)}
	updir = dir.split(/\//)[0..-2].join("/")
	dirs=[updir]+dirs if updir.length>0
	"<html><body><h3>Repertoire #{dir}</h3><hr><br>#{to_table(dirs.map {|s| "<a href='#{"/"+s}'>"+s+"/"+"</a>"})}<hr>#{to_tableb(files) {|f| [LICON[rand(LICON.size)],"<a href='#{"/"+f}'>"+File.basename(f)+"</a>",File.size(f),File.mtime(f).strftime("%d/%m/%Y %H:%M:%S")]}}</body></html>"
  end  
  def to_table(l)
	 "<table><tr>#{l.map {|s| "<td>#{s}</td>"}.join("</tr><tr>")}</tr></table>"
  end
  def to_tableb(l,&bl)
	 "<table><tr>#{l.map {|s| "<td>#{bl.call(s).join("</td><td>")}</td>"}.join("</tr><tr>")}</tr></table>"
  end
  def sendError(sock,no,txt=nil)
	 if txt
	   txt="<html><body><pre></pre>#{txt}</body></html>"
	 end
	sock.write "HTTP/1.1 #{no}/NOK\rContent-type: #{mime(".html")}\r\n\r\n <html><p>Error #{no} : #{txt}</p></html>"
  end
  def sendData(sock,type,content)
	sock.write "HTTP/1.1 200/OK\rContent-type: #{mime(type)}\r\nContent-size: #{content.size}\r\n\r\n"
	sock.write(content)
  end
  def sendFile(sock,filename)
	sock.write "HTTP/1.1 200/OK\rContent-type: #{mime(filename)}\r\nContent-size: #{File.size(filename)}\r\n\r\n"
	File.open(filename,"rb") { |f| sock.write(f.read) }
  end
  def mime(string)
	 MIME[string.split(/\./).last] || "data-octet-stream"
  end
  LICON="☀☃☎☑☑☠☣☮☺☂".split(/;/).map {|c| c+";"}
  MIME={"png" => "image/png", "gif" => "image/gif", "html" => "text/html","htm" => "text/html",
	"js" => "text/javascript" ,"css" => "text/css","jpeg" => "image/jpeg" ,"jpg" => "image/jpeg" ,
	"pdf"=> "application/pdf"   , "svg" => "image/svg+xml","svgz" => "image/svg+xml",
	"xml" => "text/xml"   ,"xsl" => "text/xml"   ,"bmp" => "image/bmp"  ,"txt" => "text/plain" ,
	"rb"  => "text/plain" ,"pas" => "text/plain" ,"tcl" => "text/plain" ,"java" => "text/plain" ,
	"c" => "text/plain" ,"h" => "text/plain" ,"cpp" => "text/plain", "xul" => "application/vnd.mozilla.xul+xml"
  } 

end # 130 loc webserver :)


Use it with :

$ws=Webserver.new(7007,1,120)


$ws.serve "/index" do |params|
 [200,".html","hello!"]
end