class Net::Server

An Internet server

Public Class Methods

new(options={}) click to toggle source
# File lib/net/server.rb, line 32
def initialize(options={})
  path = __FILE__.chomp('server.rb')
  @option_list = [[:server_name, "example.com"], [:listening_ports, ["25","486","587"]], \
    [:private_key, "#{path}server.key"], [:certificate, "#{path}server.crt"], \
    [:user_name, nil], [:group_name, nil], [:working_directory, File::realpath('.')], \
    [:pid_file, "pid"], [:daemon, false]]
  @options = options
  @option_list.each do |key,value|
    @options[key] = value if !options.has_key?(key)
  end
end

Public Instance Methods

start() click to toggle source

This is the main setup and loop.

# File lib/net/server.rb, line 164
def start
  # generate the first log messages
  LOG.info("%06d"%Process::pid) {"Starting RubyMTA at #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}, pid=#{Process::pid}"} if LOG
  LOG.info("%06d"%Process::pid) {"Options specified: #{ARGV.join(", ")}"} if LOG && ARGV.size>0

  # get the certificates, if any; they're needed for STARTTLS
  # we do this before daemonizing because the working folder might change
  $prv = if @options[:private_key] then OpenSSL::PKey::RSA.new File.read(@options[:private_key]) else nil end
  $crt = if @options[:certificate] then OpenSSL::X509::Certificate.new File.read(@options[:certificate]) else nil end

  # daemonize it if the option was set--it doesn't have to be root to daemonize it
  Process::daemon if @options[:daemon]

  # get the process ID and the user id AFTER demonizing, if that was requested
  pid = Process::pid
  uid = Process::Sys.getuid
  gid = Process::Sys.getgid
  
  LOG.info("%06d"%Process::pid) {"Daemonized at #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}, pid=#{pid}, uid=#{uid}, gid=#{gid}"} if LOG && @options[:daemon]

  # store the pid of the server session
  begin
    puts "RubyMTA running as PID=>#{pid}, UID=>#{uid}, GID=>#{gid}"
    File.open(@options[:pid_file],"w") { |f| f.write(pid.to_s) }
  rescue Errno::EACCES => e
    LOG.warn("%06d"%Process::pid) {"The pid couldn't be written. To save the pid, create a directory for '#{@options[:pid_file]}' with r/w permissions for this user."} if LOG
    LOG.warn("%06d"%Process::pid) {"Proceeding without writing the pid."} if LOG
  end

  # if ssltransportagent was started as root, make sure UserName and
  # GroupName have values because we have to drop root privileges
  # after we fork a process for the receiver
  if uid==0 # it's root
    if @options[:user_name].nil? || @options[:group_name].nil?
      LOG.error("%06d"%Process::pid) {"ssltransportagent can't be started as root unless UserName and GroupName are set."} if LOG
      exit(1)
    end
  end

  # this is the main loop which runs until admin enters ^C
  Signal.trap("INT") { raise ServerTerminate.new }
  Signal.trap("HUP") { restart if defined?(restart) }
  Signal.trap("CHLD") do
    begin
    Process.wait(-1, Process::WNOHANG)
    rescue Errno::ECHILD => e
      # ignore the error
    end
  end # trap-chld
  threads = []
  # start the server on multiple ports (the usual case)
  begin
    @options[:listening_ports].each do |port|
      threads << Thread.start(port) do |port|
        listening_thread(port)
      end
    end
    # the joins are done ONLY after all threads are started
    threads.each { |thread| thread.join }
  rescue ServerTerminate
    LOG.info("%06d"%Process::pid) {"#{@options[:server_name]} terminated by admin ^C"} if LOG
  end

  # attempt to remove the pid file
  begin
    File.delete(@options[:pid_file])
  rescue Errno::ENOENT => e
    LOG.warn("%06d"%Process::pid) {"No such file: #{e.inspect}"} if LOG
  rescue Errno::EACCES, Errno::EPERM
    LOG.warn("%06d"%Process::pid) {"Permission denied: #{e.inspect}"} if LOG
  end
end

Private Instance Methods

bind_socket(family,port,ip) click to toggle source

both the AF_INET and AF_INET6 families use this DRY method to bind to the socket.

# File lib/net/server.rb, line 82
def bind_socket(family,port,ip)
  socket = Socket.new(family, SOCK_STREAM, 0)
  sockaddr = Socket.sockaddr_in(port.to_i,ip)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(sockaddr)
  socket.listen(0)
  return socket
end
drop_root_privileges(user_name, group_name, working_directory) click to toggle source

This method drops the process's root privileges for security reasons.

# File lib/net/server.rb, line 66
def drop_root_privileges(user_name, group_name, working_directory)
  # drop root privileges
  if Process::Sys.getuid==0
    user = Etc::getpwnam(user_name)
    group = Etc::getgrnam(group_name)
    Dir.chdir(user.dir)
    Dir.chdir(working_directory) if not working_directory.nil?
    Process::GID.change_privilege(group.gid)
    Process::UID.change_privilege(user.uid)
  end
end
listening_thread(local_port) click to toggle source

The listening thread is established in this method depending on the ListenPort argument passed to it – it can be '<ipv6>/<port>', '<ipv4>:<port>', or just '<port>'.

# File lib/net/server.rb, line 95
def listening_thread(local_port)
  LOG.info("%06d"%Process::pid) {"listening on port #{local_port}..."} if LOG

  # establish an SSL context
  $ctx = OpenSSL::SSL::SSLContext.new
  $ctx.key = $prv
  $ctx.cert = $crt
  
  # check the parameter to see if it's valid
  m = /^(([0-9a-fA-F]{0,4}:{0,1}){1,8})\/([0-9]{1,5})|(([0-9]{1,3}\.{0,1}){4}):([0-9]{1,5})|([0-9]{1,5})$/.match(local_port)
  #<MatchData "2001:4800:7817:104:be76:4eff:fe05:3b18/2000" 1:"2001:4800:7817:104:be76:4eff:fe05:3b18" 2:"3b18" 3:"2000" 4:nil 5:nil 6:nil 7:nil>
  #<MatchData "23.253.107.107:2000" 1:nil 2:nil 3:nil 4:"23.253.107.107" 5:"107" 6:"2000" 7:nil>
  #<MatchData "2000" 1:nil 2:nil 3:nil 4:nil 5:nil 6:nil 7:"2000">
  case
    when !m[1].nil? # it's AF_INET6
      socket = bind_socket(AF_INET6,m[3],m[1])
    when !m[4].nil? # it's AF_INET
      socket = bind_socket(AF_INET,m[6],m[4])
    when !m[7].nil?
      socket = bind_socket(AF_INET6,m[7],"0:0:0:0:0:0:0:0")
    else
      raise ArgumentError.new(local_port)
  end # case
  ssl_server = OpenSSL::SSL::SSLServer.new(socket, $ctx);

  # main listening loop starts in non-encrypted mode
  ssl_server.start_immediately = false
  loop do
    # we can't use threads because if we drop root privileges on any thread,
    # they will be dropped for all threads in the process--so we have to fork
    # a process here in order that the reception be able to drop root privileges
    # and run at a user level--this is a security precaution--the other reason
    # to use processes is that they can be run on multiple processors
    connection = ssl_server.accept
    Process::fork do
      # now we're in the child process
      begin
        drop_root_privileges(@options[:user_name],@options[:group_name],@options[:working_directory]) if !@options[:user_name].nil?
        remote_hostname, remote_service = connection.io.remote_address.getnameinfo
        remote_ip, remote_port = connection.io.remote_address.ip_unpack
        process_call(connection, local_port, remote_port.to_s, remote_ip, remote_hostname, remote_service)
      ensure
        # here we close the child's copy of the connection --
        # since the parent already closed it's copy, this
        # one will send a FIN to the client, so the client
        # can terminate gracefully
        connection.close
        LOG.info("%06d"%Process::pid) {"Connection closed on port #{local_port} by #{@options[:server_name]}"} if LOG
        # and finally, close the child's link to the log
        LOG.close if LOG
      end
      # the child process ends here
    end # fork
    # now we're in the parent process
    # here we close the parent's copy of the connection --
    # the child (created by the Process::fork above) has another copy --
    # if this one is not closed, when the child closes it's copy,
    # the child's copy won't send a FIN to the client -- the FIN
    # is only sent when the last process holding a copy to the
    # socket closes it's copy
    connection.close
  end # loop
end
process_call(connection, local_port, remote_port, remote_ip, remote_hostname, remote_service) click to toggle source

This is the code executed after the process has been forked and root privileges have been dropped.

# File lib/net/server.rb, line 50
def process_call(connection, local_port, remote_port, remote_ip, remote_hostname, remote_service)
  begin
    Signal.trap("INT") { } # ignore ^C in the child process
    LOG.info("%06d"%Process::pid) {"Connection accepted on port #{local_port} from port #{remote_port} at #{remote_ip} (#{remote_hostname})"} if LOG

    # a new object is created here to provide separation between server and receiver
    # this call receives the email and does basic validation
    Receiver::new(connection, @options).receive(local_port, Socket::gethostname, remote_port, remote_hostname, remote_ip)
  rescue ServerQuit
    # nothing to do here
  end
end