require 'icaprb/server/version'
require 'openssl'
require 'socket'
require 'logger'
require_relative './server/request_parser'
require_relative './server/response'
require_relative './server/services'
# nodoc
module ICAPrb
# The server code of our project.
module Server
# This class contains the network related stuff like waiting for connections.
# It is the main class of this project.
class ICAPServer
# supported ICAP versions
SUPPORTED_ICAP_VERSIONS = ['1.0']
# logger for the server; default level is Logger::WARN and it writes to STDOUT
attr_accessor :logger
# services registered on the server
attr_accessor :services
# Create a new ICAP server
#
# * host the host on which the socket should be bound to
# * port the port on which the socket should be bound to - this is usually 1344
# * options when you want to use TLS, you can pass a Hash containing the following information
# :secure:: true if TLS should be used
# :certificate:: the path of the certificate
# :key:: the path of the key file
def initialize(host = 'localhost', port = 1344, options = nil)
@host, @port = host,port
@secure = false
@certificate = nil
@key = nil
if (options.is_a? Hash) && (@secure = options[:secure])
@key = options[:key]
@certificate = options[:certificate]
end
if (options.is_a? Hash) && options[:logfile]
@logger = Logger.new(options[:logfile])
else
@logger = Logger.new(STDOUT)
end
if (options.is_a? Hash) && options[:log_level]
@logger.level = options[:log_level]
else
@logger.level = Logger::WARN
end
@services = {}
@enable_tls_1_1 = options[:enable_tls_1_1] unless options.nil?
@tls_socket = false
if (options.is_a? Hash) && options[:tls_socket]
@tls_socket = options[:tls_socket]
end
end
# this methods starts the server and passes the connection to the method handle_request
# as well as the ip and the port.
# It will log the information about the connection if the level is set to info or lower.
#
# this method will most likely never crash. It is blocking so you may want to run it in
# its own thread.
def run
# run the server
server = create_server
loop do
Thread.start(server.accept) do |connection|
if connection.is_a? OpenSSL::SSL::SSLSocket
port, ip = Socket.unpack_sockaddr_in(connection.io.getpeername)
else
port, ip = Socket.unpack_sockaddr_in(connection.getpeername)
end
@logger.info "[CONNECT] Client from #{ip}:#{port} connected to this server"
begin
until connection.closed? do
handle_request(connection,ip)
end
rescue Errno::ECONNRESET => e
@logger.error "[CONNECTION ERROR] Client #{ip}:#{port} got disconnected (CONNECTION RESET BY PEER): #{e}"
end
end
end
end
# this method handles the connection to the client. It will call the parser and sends the request to the service.
# The service must return anything and handle the request. The important classes are in response.rb
# This method includes a lot of error handling. It will respond with an error page if
# * The ICAP version is not supported
# * It cannot read the header
# * The method is not supported by the service
# * The request has an upgrade header, which is not supported
# * the client requested an upgrade to tls, but the server has not been configured to use it
# * the client requested a service, which does not exist
def handle_request(connection, ip)
# handles the request
begin
parser = RequestParser.new(connection, ip, self)
parsed_data = parser.parse
rescue Exception => e
#puts $@
logger.error "[PARSER ERROR] Error while parsing request - Error Message is: #{e}"
Response.display_error_page(connection,400,
{http_version: '1.0',http_status: 400, 'title' => 'Invalid Request',
'content' => 'Your client sent a malformed request - please fix it and try it again.'})
return
end
unless SUPPORTED_ICAP_VERSIONS.include? parsed_data[:icap_data][:request_line][:version]
Response.display_error_page(connection,505,
{http_version: '1.0',
http_status: 500,
'title' => 'Unknown ICAP-version used',
'content' => 'We are sorry but your ICAP version is not known by this server.'})
end
# send the data to the service framework
path = parsed_data[:icap_data][:request_line][:uri].path
path = path[1...path.length] if path != '*'
if (service = @services[path])
icap_method = parsed_data[:icap_data][:request_line][:icap_method]
if icap_method == :options
return service.generate_options_response(connection)
else
if service.supported_methods.include? icap_method
service.do_process(self,ip,connection,parsed_data)
return
else
Response.display_error_page(connection,405,
{http_version: '1.0',http_status: 500, 'title' => 'ICAP Error',
'content' => 'Your client accessed the service with the wrong method.'})
end
end
elsif (path == '*') && (parsed_data[:icap_data][:request_line][:icap_method] == :options)
# check for an upgrade header
icap_data = parsed_data[:icap_data]
if icap_data[:header]['Connection'] == 'Upgrade' && connection.class == OpenSSL::SSL::SSLSocket
case icap_data[:header]['Upgrade']
when /^TLS\/[\d\.]+, ICAP\/[\d\.]+$/
response = Response.new
response.icap_status_code = 101
response.icap_header['Upgrade'] = "TLS/1.2, ICAP/#{icap_data[:request_line][:version]}"
response.write_headers_to_socket connection
connection.accept # upgrade connection to use tls
else
Response.display_error_page(connection,400,{'title' => 'ICAP Error',
'content' => 'Upgrade header is missing',
:http_version => '1.1',
:http_status => 500})
end
else
Response.display_error_page(connection,500,{'title' => 'ICAP Error',
'content' => 'This server has no TLS support.',
:http_version => '1.1',
:http_status => 500})
end
return
else
Response.display_error_page(connection,404,
{http_version: '1.0',http_status: 500, 'title' => 'Not Found',
'content' => 'Sorry, but the ICAP service does not exist.'})
return
end
end
private
# this method will create a server based on the information we got on initialisation.
# It will create an +TCPServer+ with the host and port given at initialisation.
# If @secure evaluates to true, a +SSLServer+ will be crated and wraps this +TCPServer+.
# By default, only TLS 1.2 is supported for security reasons but TLS 1.1 can be enabled
# as well when the option is set at initialization.
# For security reasons, the encryption algorithms +RC4+ and +DES+ are disabled as well as the
# digest algorithm +SHA1+.
# returns: An instance of TCPServer or SSLServer
def create_server
tcp_server = TCPServer.new(@host, @port)
if @secure
ctx = OpenSSL::SSL::SSLContext.new(:TLSv1_2_server)
ctx.cert = OpenSSL::X509::Certificate.new(File.read(@certificate))
ctx.key = OpenSSL::PKey::RSA.new(File.read(@key))
# secure OpenSSL
###############################
# do not allow ssl v2 or ssl v3
ctx.options |= (OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | OpenSSL::SSL::OP_NO_TLSv1)
# disable TLS 1.1 unless the user requests it
ctx.options |= OpenSSL::SSL::OP_NO_TLSv1_1 unless @enable_tls_1_1
# I do not want to have something encrypted with RC4 or with a DES variant and it should not use the digest
# algorithm SHA1
ctx.ciphers =
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers].split(':').select do |cipher_suite|
!((cipher_suite =~ /RC4|DES/) || (cipher_suite =~ /SHA$/))
end.join(':')
tcp_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
tcp_server.start_immediately = @tls_socket # requires accept call later
end
@tcp_server = tcp_server
end
end
end
end