#!/usr/bin/env ruby $VERSION = '0.0.1' $DEBUG = false 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[: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.length > 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.length].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 FileServer < EventParsers::Http11Parser::Request # 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) content_type = MIME.check(filename).type file_body = File.read(filename) # If .markdown, render as Markdown if file_extension == 'markdown' file_body = Renderers::Markdown.render_content(file_body) content_type = 'text/html' end # If .textile, render as Textile if file_extension == 'textile' file_body = Renderers::Textile.render_content(file_body) content_type = 'text/html' end # Send Response respond!('200 Ok', content_type, file_body) 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) # Convert to UTF-8 if possible chardet = UniversalDetector::chardet(body) if chardet['confidence'] > 0.7 charset = chardet['encoding'] body = Iconv.conv('utf-8', charset, body) # else # no conversion end body_length = body.length # 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}\r\n" span = (Time.now - connection.time).to_f content_type 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 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