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