lib/ftpd/server.rb
# frozen_string_literal: true
module Ftpd
class Server
include Memoizer
# The interface to bind to (e.g. "127.0.0.1", "0.0.0.0",
# "10.0.0.12", "::1", "::", etc.). Defaults to "127.0.0.1"
#
# Set this before calling #start.
#
# @return [String]
attr_accessor :interface
# The port to bind to. Defaults to 0, which causes an ephemeral
# port to be used. When bound to an ephemeral port, use
# #bound_port to find out which port was actually bound to.
#
# Set this before calling #start.
#
# @return [String]
attr_accessor :port
def initialize
@interface = '127.0.0.1'
@port = 0
@stopping = false
@server_thread = nil
end
# The port the server is bound to. Must not be called until after
# #start is called.
#
# @return [Integer]
def bound_port
@server_socket.addr[1]
end
# The calling thread will suspend execution until the server is
# stopped.
def join
raise 'Server is not started!' if @server_thread.nil?
@server_thread.join
end
# Start the server. This creates the server socket, and the
# thread to service it.
def start
@server_socket = make_server_socket
@server_thread = make_server_thread
end
# Stop the server. This closes the server socket, which in turn
# stops the thread.
def stop
@stopping = true
begin
@server_socket.shutdown
rescue Errno::ENOTCONN
end
@server_socket.close
end
private
def make_server_socket
return TCPServer.new(@interface, @port)
end
def make_server_thread
Thread.new do
Thread.abort_on_exception = true
loop do
begin
begin
socket = accept
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL
IO.select([@server_socket])
sleep(0.2)
retry
rescue Errno::EBADF, Errno::ENOTSOCK
raise unless @stopping
@stopping = false
break
end
start_session socket
rescue IOError
break
end
end
end
end
def start_session(socket)
if allow_session?(socket)
start_session_thread socket
else
deny_session socket
close_socket socket
end
end
def allow_session?(socket)
true
end
def deny_session socket
end
def start_session_thread(socket)
Thread.new do
begin
session socket
rescue OpenSSL::SSL::SSLError
ensure
close_socket socket
end
end
end
def accept
@server_socket.accept
end
def close_socket(socket)
if socket.respond_to?(:shutdown)
socket.shutdown rescue nil
socket.read rescue nil
end
ensure
socket.close
end
end
end