#!/usr/bin/env ruby $VERSION = File.read(File.dirname(__FILE__) + '/../VERSION').chomp $DEBUG = false if $DEBUG require 'benchmark' require 'rubygems' require 'ruby-debug' end def log(*msgs) msgs.each do |msg| $stdout << msg end $stdout.flush true end require 'socket' $options = {} ENV['XDG_DATA_DIRS'] = (ENV['XDG_DATA_DIRS'].to_s.split(/:/) << '/opt/local/share').join(':') require 'optparse' optparse = OptionParser.new do |opts| opts.banner = "Usage: #{$0} [options]" $options[:port] = 80 opts.on( '-p PORT', '--port PORT', "Run on PORT. Defaults to #{$options[:port]}" ) do |port| $options[:port] = port.to_i end # Gets the IP address for the default # $options[:address] = Socket.getaddrinfo(Socket.gethostname, nil, 'AF_INET')[0][2] $options[:address] = '0.0.0.0' opts.on( '-a ADDRESS', '--address ADDRESS', "Listen on ADDRESS ip. Defaults to [#{$options[:address]}]") do |address| $options[:address] = address end $options[:cache_size] = 1024 # default is to use a 1MB cache opts.on( '--cache-size SIZE', "Turn in-memory caching on and set the maximum cache size. Example: 500K, 10M, 1G") do |size| $options[:cache_size] = case size when /^\d+$/ size.to_i when /^\d+kb?$/i size.to_i * 1024 # kilobytes when /^\d+mb?$/i size.to_i * 1024*1024 # megabytes when /^\d+gb?$/i size.to_i * 1024*1024*1000 # gigabytes else warn "Couldn't understand --cache-size #{size}!" exit end end $options[:https_domain] = $options[:address] end optparse.parse! require 'socket' require 'openssl' class ::OpenSSL::SSL::SSLSocket alias :read_nonblock :readpartial end class EventMachineMini DEFAULT_SSL_OPTIONS = Hash.new do |h,k| case k when :SSLCertificate h[k] = OpenSSL::X509::Certificate.new(File.read(h[:SSLCertificateFile])) when :SSLPrivateKey h[k] = OpenSSL::PKey::RSA.new(File.read(h[:SSLPrivateKeyFile])) else nil end end DEFAULT_SSL_OPTIONS.merge!( :GenerateSSLCert => false, :ServerSoftware => "Ruby TCP Router OpenSSL/#{::OpenSSL::OPENSSL_VERSION.split[1]}", :SSLCertName => [['CN', $options[:https_domain]]], :SSLCertComment => "Generated by Ruby/OpenSSL", :SSLCertificateFile => 'cert.pem', :SSLPrivateKeyFile => 'key.pem', :SSLClientCA => nil, :SSLExtraChainCert => nil, :SSLCACertificateFile => 'cacert.pem', :SSLCACertificatePath => nil, :SSLCertificateStore => nil, :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_PEER, :SSLVerifyDepth => 1, :SSLVerifyCallback => nil, # custom verification :SSLTimeout => nil, :SSLOptions => nil, :SSLStartImmediately => true ) class << self def ssl_config(config=DEFAULT_SSL_OPTIONS) @ssl_config ||= config end end def running? Thread.current[:status] == :running end def stop! Thread.current[:status] = :stop end attr_reader :router def initialize(routes={}) @router = {} # Set up the listening sockets (routes[:listen] || {}).each do |ip_port,instantiate_klass| ip, port = ip_port.split(/:/) socket = TCPServer.new(ip, port) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) @router[socket] = instantiate_klass # puts "Listening on #{ip_port} for #{instantiate_klass.name} messages..." end # Set up the listening SSL sockets (routes[:ssl_listen] || {}).each do |ip_port,instantiate_klass| ip, port = ip_port.split(/:/) socket = TCPServer.new(ip, port) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) ssl_socket = ::OpenSSL::SSL::SSLServer.new(socket, ssl_context) ssl_socket.start_immediately = self.class.ssl_config[:SSLStartImmediately] @router[ssl_socket] = instantiate_klass # puts "Listening on #{ip_port} (SSL) for #{instantiate_klass.name} messages..." end # Set up the connect sockets (routes[:connect] || {}).each do |ip_port,args| args = [args] unless args.is_a?(Array); instantiate_klass = args.shift ip, port = ip_port.split(/:/) socket = TCPSocket.new(ip, port) clients[socket] = instantiate_klass.new(self,socket,*args) # puts "Connecting to #{ip_port} for #{instantiate_klass.name} messages..." end end def run Thread.current[:status] = :running # trap("INT") { stop! } # This will end the event loop within 0.5 seconds when you hit Ctrl+C loop do # log "tick #{Thread.current[:status]}\n" if $DEBUG # Clean up any closed clients clients.each_key do |sock| if sock.closed? conn = clients.delete(sock) conn.upon_unbind if conn.respond_to?(:upon_unbind) if conn.respond_to?(:key) connections_by_key.delete(conn.key) rescue nil end end end if !running? unless listen_sockets.empty? # puts "Closing all listening ports." shutdown_listeners! end if client_sockets.empty? # It's the next time around after we closed all the client connections. break else # puts "Closing all client connections." close_all_clients! end end # puts "Listening to #{listen_sockets.length} sockets: #{listen_sockets.inspect}" begin event = select(listen_sockets + client_sockets,nil,nil,0.5) rescue IOError next end if event.nil? # nil would be a timeout, we'd do nothing and start loop over. Of course here we really have no timeout... @router.values.each { |klass| klass.tick if klass.respond_to?(:tick) } else event[0].each do |sock| # Iterate through all sockets that have pending activity # puts "Event on socket #{sock.inspect}" if listen_sockets.include?(sock) # Received a new connection to a listening socket sock = accept_client(sock) clients[sock].upon_new_connection if clients[sock].respond_to?(:upon_new_connection) else # Activity on a client-connected socket if sock.eof? # Socket's been closed by the client log "Connection #{clients[sock].to_s} was closed by the client.\n" sock.close clients[sock].upon_unbind if clients[sock].respond_to?(:upon_unbind) client = clients[sock] clients.delete(sock) else # Data in from the client catch :stop_reading do # puts "Reading data from socket #{sock.inspect} / #{clients[sock].inspect}" begin if sock.respond_to?(:read_nonblock) # puts "read_nonblock" 10.times { data = sock.read_nonblock(4096) clients[sock].receive_data(data) } else # puts "sysread" data = sock.sysread(4096) clients[sock].receive_data(data) end rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError => e # no-op. This will likely happen after every request, but that's expected. It ensures that we're done with the request's data. rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ECONNREFUSED, IOError => e log "Closed Err: #{e.inspect}\n" sock.close clients[sock].upon_unbind if clients[sock].respond_to?(:upon_unbind) end end end end end end end end def clients @clients ||= {} end def connections_by_key @connections_by_key ||= {} end def close_all_clients! # puts "Closing #{client_sockets.length} client connections..." client_sockets.each { |socket| socket.close rescue nil } end def shutdown_listeners! # puts "Shutting down #{listen_sockets.length} listeners..." listen_sockets.each { |socket| socket.close rescue nil } end private def listen_sockets @router.keys.select {|socket| !socket.closed? } end def accept_client(source_socket) client_socket = source_socket.accept connection = @router[source_socket].new(self,client_socket) clients[client_socket] = connection end def client_sockets @clients.keys end def ssl_context unless @ssl_context @ssl_context = OpenSSL::SSL::SSLContext.new @ssl_context.client_ca = self.class.ssl_config[:SSLClientCA] @ssl_context.ca_file = self.class.ssl_config[:SSLCACertificateFile] @ssl_context.ca_path = self.class.ssl_config[:SSLCACertificatePath] @ssl_context.extra_chain_cert = self.class.ssl_config[:SSLExtraChainCert] @ssl_context.cert_store = self.class.ssl_config[:SSLCertificateStore] @ssl_context.verify_mode = self.class.ssl_config[:SSLVerifyClient] @ssl_context.verify_depth = self.class.ssl_config[:SSLVerifyDepth] @ssl_context.verify_callback = self.class.ssl_config[:SSLVerifyCallback] @ssl_context.timeout = self.class.ssl_config[:SSLTimeout] @ssl_context.options = self.class.ssl_config[:SSLOptions] if self.class.ssl_config[:GenerateSSLCert] puts "Generating SSL Certificate..." @ssl_context.key = OpenSSL::PKey::RSA.generate(4096) ca = OpenSSL::X509::Name.parse("/C=US/ST=Michigan/O=BehindLogic/CN=behindlogic.com/emailAddress=cert@desktopconnect.com") cert = OpenSSL::X509::Certificate.new cert.version = 2 cert.serial = 1 cert.subject = ca cert.issuer = ca cert.public_key = @ssl_context.key.public_key cert.not_before = Time.now cert.not_after = Time.now + 3600 # this http session should last no longer than 1 hour @ssl_context.cert = cert else @ssl_context.cert = self.class.ssl_config[:SSLCertificate] @ssl_context.key = self.class.ssl_config[:SSLPrivateKey] end end @ssl_context end end require 'base64' module EventParsers # Implement this by including it in a class and call receive_data on every read event. # Callbacks available: # upon_new_request(request) # after first HTTP line # receive_header(request, header) # after each header is received # upon_headers_finished(request) # after all headers are received # process_request(request) # after the full request is received module Http11Parser module BasicAuth ::UnparsableBasicAuth = Class.new(RuntimeError) class << self def parse(basic_auth_string) # Do the special decoding here if basic_auth_string =~ /^Basic (.*)$/ auth_string = $1 auth_plain = Base64.decode64(auth_string) return auth_plain.split(/:/,2) else warn "Bad Auth string!" raise UnparsableBasicAuth end end end def initialize(username, password) @username = username @password = password end def to_s # Do the special encoding here end end class HeaderAndEntityStateStore attr_accessor :state, :delimiter, :linebuffer, :textbuffer, :entity_size, :entity_pos, :bogus_lines def initialize(state, delimiter) @state = state @delimiter = delimiter reset! end def reset! @linebuffer = [] @textbuffer = [] @entity_size = nil @entity_pos = 0 @bogus_lines = 0 end def entity? !entity_size.nil? && entity_size > 0 end def bogus_line!(ln=nil) log "bogus line:\n#{ln}\n" if ln && $DEBUG @bogus_lines += 1 end end class Request attr_accessor :connection, :method, :http_version, :resource_uri, :query_params, :headers, :entity, :response def ready! @ready = true end def ready? !!@ready end def initialize(request_params={}) @http_version = request_params[:http_version] if request_params.has_key? :http_version @resource_uri = request_params[:resource_uri] if request_params.has_key? :resource_uri @headers = request_params[:headers] if request_params.has_key? :headers @entity = request_params[:entity] if request_params.has_key? :entity @headers ||= {} @entity ||= '' end def inspect "HTTP Request:\n\t#{@method} #{@resource_uri} HTTP/#{@http_version}\n\t#{@headers.map {|k,v| "#{k}: #{v}"}.join("\n\t")}\n\n\t#{@entity.to_s.gsub(/\n/,"\n\t")}" end def has_entity? @entity != '' end def basic_auth BasicAuth.parse(@headers['authorization']) if @headers['authorization'] end def params query_params end end def self.included(base) base.extend ClassMethods end module ClassMethods def request_klass(klass=nil) @request_klass ||= (klass || Request) end end attr_reader :socket def request_backlog @request_backlog ||= [] end def current_request request_backlog.first end def parsing_request request_backlog.last end HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i HttpRequestRE = /^(GET|POST|PUT|DELETE) (\/.*) HTTP\/([\d\.]+)[\r\n]?$/i BlankLineRE = /^[\n\r]+$/ def receive_data(data) return unless (data and data.size > 0) @last_activity = Time.now case parser.state when :init if ix = data.index("\n") parser.linebuffer << data[0...ix+1] ln = parser.linebuffer.join parser.linebuffer.clear log "[#{parser.state}]: #{ln}" if $DEBUG if ln =~ HttpRequestRE request_backlog << self.class.request_klass.new parsing_request.connection = self method, resource_uri, http_version = parse_init_line(ln) parsing_request.method = method parsing_request.resource_uri = resource_uri parsing_request.query_params = parse_query_string(resource_uri.index('?') ? resource_uri[(resource_uri.index('?')+1)..-1] : '') parsing_request.http_version = http_version upon_new_request(parsing_request) if respond_to?(:upon_new_request) parser.state = :headers else parser.bogus_line!(ln) end receive_data(data[(ix+1)..-1]) else parser.linebuffer << data end when :headers if ix = data.index("\n") parser.linebuffer << data[0...ix+1] ln = parser.linebuffer.join parser.linebuffer.clear # If it's a blank line, move to content state if ln =~ BlankLineRE upon_headers_finished(parsing_request) if respond_to?(:upon_headers_finished) if parser.entity? # evaluate_headers(parsing_request.headers) parser.state = :entity else receive_full_request end else header = parse_header_line(ln) log "\t[#{parser.state}]: #{header.inspect}\n" if $DEBUG receive_header(parsing_request, header.to_a[0]) if respond_to?(:receive_header) parsing_request.headers.merge!(header) end receive_data(data[(ix+1)..-1]) else parser.linebuffer << data end when :entity if parser.entity_size chars_yet_needed = parser.entity_size - parser.entity_pos taking_this_many = [chars_yet_needed, data.size].sort.first parser.textbuffer << data[0...taking_this_many] leftover_data = data[taking_this_many..-1] parser.entity_pos += taking_this_many if parser.entity_pos >= parser.entity_size entity_data = parser.textbuffer.join parser.textbuffer.clear parsing_request.entity << entity_data log "[#{parser.state}]: #{entity_data}\n" if $DEBUG receive_full_request end receive_data(leftover_data) else raise "TODO!" # receive_binary_data data end else # Probably shouldn't ever be here? raise "Shouldn't be here!" end # TODO: Exception if number of parser.bogus_lines is higher than threshold end def process_request(request) warn "STUB - overwrite process_request in a subclass of Http11Parser to process this #{request.inspect}" end def send_response!(http_response) log "Sending Response: #{http_response.inspect}\n" if $DEBUG socket.write(http_response) request_backlog.shift # Process the next request IF it is already waiting. process_request(current_request) if current_request && current_request.ready? end private def parse_init_line(ln) method, resource_uri, http_version = ln.match(HttpRequestRE).to_a[1..-1] # TODO: Exception if the request is improper! [method, resource_uri, http_version] end def parse_query_string(qstring) params = (CGI.parse(qstring) || {}) rescue {} params.inject({}) do |h,(k,v)| h[k.to_sym] = (v.is_a?(Array) && v.length == 1) ? v[0] : v h end end def parse_header_line(ln) ln.chomp! if ln =~ /:/ name,value = ln.split(/:\s*/,2) if name.downcase == 'content-length' parser.entity_size = Integer(value.gsub(/\D/,'')) # TODO: Exception if content-length specified is too big end {name.downcase => value} else parser.bogus_line!(ln) {} end end def receive_full_request parsing_request.ready! parser.state = :init # Process this request now that we're ready -- IF any previous requests are already responded to. # Otherwise, this request will be waiting, and when the previous one(s) are responded to, this one # will be triggered next. process_request(parsing_request) if parsing_request == current_request log "Processed request. Prepared for next request.\n\n" if $DEBUG parser.reset! end def parser @parser ||= HeaderAndEntityStateStore.new(:init, "\n") end end end require 'iconv' require 'rubygems' module Renderers autoload :Markdown, 'httphere/markdown' autoload :Textile, 'httphere/textile' end require 'UniversalDetector' require 'shared-mime-info' class File def size File.size(path) end end class FileServer < EventParsers::Http11Parser::Request class << self attr_accessor :cache def cached?(filename) # Serve from cache if it's in the cache and if the file hasn't changed. if cache # Delete from cache if the file's been modified since last cache. cache.delete(filename) if cache.has_key?(filename) && File.mtime(filename) > cache[filename][0][:mtime] cache.has_key?(filename) end end def from_cache(filename) cache[filename][0][:last_accessed_at] = Time.now puts "From cache: #{filename} / #{cache[filename][0][:size]}" [cache[filename][1], cache[filename][0][:content_type]] end def cache?(filename,response_body) # Obviously, don't cache if it's bigger than the cache max size cache && File.exists?(filename) && response_body.size < cache.max_size end def cache!(filename,content_type,response_body) if cache?(filename,response_body) puts "Caching #{filename} / #{response_body.size}" cache[filename] = [ { :last_accessed_at => Time.now, :mtime => File.mtime(filename), :size => response_body.size, :content_type => content_type }, response_body ] end end end # This is where the routing is processed. def process # Get the filename desired filename, query = resource_uri.split('?',2) @filename = filename.sub(/^\//,'') # Default to any file named index.* @filename = Dir["index.*"].first if @filename.to_s == '' && Dir["index.*"].length > 0 file_extension = (filename.match(/\.([^\.]+)$/) || [])[1] if File.exists?(@filename) && !File.directory?(@filename) # Read from cache if possible file_handle = FileServer.cached?(@filename) ? :from_cache : :from_disk if file_handle == :from_disk if $DEBUG Benchmark.bm do |x| x.report("Getting MIME type") do @content_type = MIME.check(@filename).type end end else @content_type = MIME.check(@filename).type end file_handle = File.open(@filename) # If .markdown, render as Markdown if file_extension == 'markdown' file_handle = Renderers::Markdown.render_content(file_handle.read) @content_type = 'text/html' end # If .textile, render as Textile if file_extension == 'textile' file_handle = Renderers::Textile.render_content(file_handle.read) @content_type = 'text/html' end end # Send Response respond!('200 Ok', @content_type, file_handle) else respond!('404 Not Found', 'text/plain', "Could not find file: '#{resource_uri}'") end end def respond!(status, content_type, body) respond(status, content_type, body) connection.halt! end def respond(status, content_type, body) if body == :from_cache body, content_type = FileServer.from_cache(@filename) else body = StringIO.new(body.to_s) if !body.is_a?(IO) && body.respond_to?(:to_s) # Convert to UTF-8 if possible chardet = UniversalDetector::chardet(body.read(512)) rescue {'confidence' => 0.8, 'encoding' => 'utf-8'}; body.rewind # Detect charset only from the first 512 bytes if chardet['confidence'] > 0.7 && !['utf-8', 'ascii'].include?(chardet['encoding']) if $DEBUG Benchmark.bm do |x| x.report("Converting from #{chardet['encoding']} to UTF-8") do charset = chardet['encoding'] body = StringIO.new(Iconv.conv('utf-8', charset, body.read)) end end else charset = chardet['encoding'] body = StringIO.new(Iconv.conv('utf-8', charset, body.read)) end else # no conversion puts "No charset conversion necessary." if $DEBUG end # Write to cache if we should FileServer.cache!(@filename, content_type, body) end body_length = body.size body.rewind # Send the response! connection.send_response! "HTTP/1.1 #{status}\r\nServer: HTTP-Here, version #{$VERSION}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body_length+2}\r\n\r\n#{body.read}\r\n" span = (Time.now - connection.time).to_f puts (status =~ /200/ ? "Served #{resource_uri} (#{content_type})" : "404 #{resource_uri}" ) + " at #{1 / span} requests/second" unless status =~ /404/ && resource_uri == '/favicon.ico' end end # Requires that a module includes this and defines an initialize method that defines @options class Http11Server include EventParsers::Http11Parser request_klass FileServer attr_reader :time def initialize(server,socket) @socket = socket @time = Time.now end # These just pass the call to the request to handle itself def upon_new_request(request) request.upon_new_request if request.respond_to?(:upon_new_request) end def upon_headers_finished(request) request.upon_headers_finished if request.respond_to?(:upon_headers_finished) end def process_request(request) request.process end def halt! socket.close throw :stop_reading end def upon_unbind # puts "Client #{@socket} disconnected." end end # EventMachineMini.ssl_config[:GenerateSSLCert] = true class Cache def initialize(config={}) @config = config @cache = {} end def max_size @config[:max_size] end def [](key) @cache[key] end def []=(key,value) @cache[key] = value end def has_key?(key) @cache.has_key?(key) end def delete(key) @cache.delete(key) end def bytes @cache.values.inject(0) {|s,v| s+v[0][:size]} end end # Turn Caching on if asked for if $options[:cache_size] FileServer.cache = Cache.new(:max_size => $options[:cache_size]) puts "Caching files in memory, using up to #{$options[:cache_size]} bytes." end puts "HTTP Here v#{$VERSION} : listening on #{$options[:address]}:#{$options[:port]}..." begin $server = EventMachineMini.new( :listen => {"#{$options[:address]}:#{$options[:port]}" => Http11Server} ) rescue Errno::EACCES puts "\tLooks like you don't have permission to use that port!" exit end Kernel.trap(:INT) { $server.stop! } $server.run puts " HTTP Here Finished\n" exit