Class: Server
- Includes:
- Socket::Constants
- Defined in:
- lib/rubymta/server.rb
Overview
MAIN server class – starts at Server#start
Instance Method Summary collapse
-
#bind_socket(family, port, ip) ⇒ Object
both the AF_INET and AF_INET6 families use this DRY method.
-
#drop_root_privileges ⇒ Object
this method drops the process’s root privileges for security reasons.
-
#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>’.
-
#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.
-
#process_options ⇒ Object
this method parses the command line options.
-
#start ⇒ Object
process_options.
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_privileges ⇒ Object
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_options ⇒ Object
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 = OpenStruct.new .log = Logger::INFO .daemonize = false begin OptionParser.new do |opts| opts.on("--debug", "Log all messages") { |v| .log = Logger::DEBUG } opts.on("--info", "Log all messages") { |v| .log = Logger::INFO } opts.on("--warn", "Log all messages") { |v| .log = Logger::WARN } opts.on("--error", "Log all messages") { |v| .log = Logger::ERROR } opts.on("--fatal", "Log all messages") { |v| .log = Logger::FATAL } opts.on("--daemonize", "Run as system daemon") { |v| .daemonize = true } end.parse! rescue OptionParser::InvalidOption => e LOG.warn("%06d"%Process::pid) {"#{e.inspect}"} end end |
#start ⇒ Object
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 = 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 |