Class: Server

Inherits:
Object show all
Includes:
Socket::Constants
Defined in:
lib/rubymta/server.rb

Overview

MAIN server class – starts at Server#start

Instance Method Summary collapse

Instance Method Details

#bind_socket(family, port, ip) ⇒ Object

both the AF_INET and AF_INET6 families use this DRY method



138
139
140
141
142
143
144
145
# 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_privilegesObject

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



129
130
131
132
133
134
135
# 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) ⇒ Object

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>’



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# 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) ⇒ Object

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



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# 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_optionsObject

this method parses the command line options



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 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

#startObject

process_options



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# 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