class Server

MAIN server class – starts at Server#start

Public Instance Methods

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

both the AF_INET and AF_INET6 families use this DRY method

# File lib/rubymta/server.rb, line 138
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() click to toggle source

this method drops the process's root privileges for security reasons

# File lib/rubymta/server.rb, line 129
def drop_root_privileges
  if Process::Sys.getuid==0
    Dir.chdir($app[:path]) if not $app[:path].nil?
    Process::GID.change_privilege($app[:ginfo].gid)
    Process::UID.change_privilege($app[:uinfo].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/rubymta/server.rb, line 149
def listening_thread(local_port)
  LOG.info("%06d"%Process::pid) {"listening on port #{local_port}..."}
  
  # 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? # its AF_INET6
      socket = bind_socket(AF_INET6,m[3],m[1])
    when !m[4].nil? # its 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
  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
    connection = ssl_server.accept
    Process::fork do
      begin
        drop_root_privileges if !UserName.nil?
        begin
          remote_hostname, remote_service = connection.io.remote_address.getnameinfo
        rescue SocketError => e
          LOG.info("%06d"%Process::pid) { e.to_s }
          remote_hostname, remote_service = "(none)", nil
        end
        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)
        LOG.info("%06d"%Process::pid) {"Connection closed on port #{local_port} by #{ServerName}"}
      rescue Errno::ENOTCONN => e
        LOG.info("%06d"%Process::pid) {"Remote Port scan on port #{local_port}"}
      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
        # and finally, close the child's link to the log
        LOG.close
      end
    end
    # 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
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/rubymta/server.rb, line 111
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})"}

    # 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).receive(local_port, Socket::gethostname, remote_port, remote_hostname, remote_ip)
  rescue Quit
    # nothing to do here
  ensure
    # close the database (the child's copy)
    S3DB.disconnect if S3DB
    nil # don't return the Receiver object
  end
end
process_options() click to toggle source

this method parses the command line options

# File lib/rubymta/server.rb, line 212
def process_options
  options = OpenStruct.new
  options.log = Logger::INFO
  options.daemonize = false
  begin
    OptionParser.new do |opts|
      opts.on("--debug",  "Log all messages")     { |v| options.log = Logger::DEBUG }
      opts.on("--info",   "Log all messages")     { |v| options.log = Logger::INFO }
      opts.on("--warn",   "Log all messages")     { |v| options.log = Logger::WARN }
      opts.on("--error",  "Log all messages")     { |v| options.log = Logger::ERROR }
      opts.on("--fatal",  "Log all messages")     { |v| options.log = Logger::FATAL }
      opts.on("--daemonize", "Run as system daemon") { |v| options.daemonize = true }
    end.parse!
  rescue OptionParser::InvalidOption => e
    LOG.warn("%06d"%Process::pid) {"#{e.inspect}"}
  end
  options
end
start() click to toggle source
# File lib/rubymta/server.rb, line 231
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}"}
  LOG.info("%06d"%Process::pid) {"Options specified: #{ARGV.join(", ")}"} if ARGV.size>0

  # get the options from the command line
  @options = process_options
  LOG.level = @options.log

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

  # establish an SSL context for use in `listening_thread`
  $ctx = OpenSSL::SSL::SSLContext.new
  $ctx.key = $prv
  $ctx.cert = $crt

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

  # 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 @options.daemonize

  # store the pid of the server session
  begin
    LOG.info("%06d"%Process::pid) {"RubyMTA running as PID=>#{pid}, UID=>#{uid}, GID=>#{gid}"}
    File::open("#{PidPath}/rubymta.pid","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 '#{PidPath}' with r/w permissions for this user."}
    LOG.warn("%06d"%Process::pid) {"Proceeding without writing the pid."}
  end

  # if rubymta 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 UserName.nil? || GroupName.nil?
      LOG.error("%06d"%Process::pid) {"rubymta can't be started as root unless UserName and GroupName are set."}
      exit(1)
    end
  end

  # this is the main loop which runs until admin enters ^C
  Signal.trap("INT") { raise Terminate.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
  threads = []
  # start the server on multiple ports (the usual case)
  begin
    ListeningPorts.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 Terminate
    LOG.info("%06d"%Process::pid) {"#{ServerName} terminated by admin ^C"}
  end

ensure

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

  # close the log
  LOG.close if LOG
end