#!/usr/local/bin/ruby
require "socket"
require "syslog"
require "optconfig"

options = {
  "socket"      => [true, "/var/run/saslauthd/mux"],
  "socket-mode" => [true, "0777"],
  "auth"        => true,
  "syslog"      => [true, "mail"],
  "pid-file"    => [true, "/var/run/saslauthd/pid"],
  "help"        => nil,
}

def usage()
  STDERR.puts <<EOS
saslauthd [options]
  options:
    --socket=filename   : socket filename
    --socket-mode=nnn   : socket file mode (000-777)
    --syslog=facility   : syslog faiclity
    --pid-file=filename : pid filename
EOS
  exit 1
end

def main()
  fac = "LOG_#{$opt["syslog"].upcase}"
  unless Syslog.constants.include? fac then
    STDERR.puts "unknown facility: #{$opt["syslog"]}"
    exit 1
  end

  facility = eval "Syslog::#{fac}"
  Syslog.open(File.basename($0), nil, facility)

  Signal.trap(:TERM) do
    exit
  end

  Signal.trap(:HUP) do
  end

  exit if fork
  Process.setsid
  File.open("/dev/null"){|f| STDIN.reopen f}
  File.open("/dev/null", "w"){|f| STDOUT.reopen f}
  File.open("/dev/null", "w"){|f| STDERR.reopen f}

  begin
    Syslog.notice("start")
    sock = UNIXServer.new($opt["socket"])
    File.chmod($opt["socket-mode"].oct, $opt["socket"])
    socket_file = $opt["socket"]
    File.open($opt["pid-file"], File::WRONLY|File::CREAT|File::EXCL, 0600){|f| f.puts $$}
    pid_file = $opt["pid-file"]
    loop do
      begin
        s = sock.accept
      rescue Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET
        next
      end
      Thread.new(s) do |s2|
        begin
          login = read_packet(s2)
          password = read_packet(s2)
          service = read_packet(s2)
          realm = read_packet(s2)
          result = auth(login, password, service, realm)
          write_packet(s2, result)
        rescue
          Syslog.err("%s", $!)
        end
      end
    end
  rescue
    Syslog.err("%s", $!)
  ensure
    File.unlink socket_file rescue nil if socket_file
    File.unlink pid_file rescue nil if pid_file
    Syslog.notice("end")
  end
end

def read_packet(s)
  n = s.read(2)
  if n.nil? or n.length != 2 then
    raise "unexpected EOF"
  end
  n = n.unpack("n")[0]
  str = s.read(n)
  if str.nil? or str.length != n then
    raise "unexpected EOF"
  end
  return str
end

def write_packet(s, str)
  if str.length > 65535 then
    raise "too large data: #{str[0,50]}..."
  end
  s.write([str.length].pack("n")+str)
end

def auth(login, password, service, realm)
  Syslog.info("dummy: login=%s, service=%s, realm=%s", login, service, realm)
  "NO"
end

begin
  $opt = OptConfig.new
  $opt.options = options
  $opt.file = "#{File.dirname($0)}/saslauthd.conf"
  n = $opt.parse ARGV
  if n < ARGV.size or $opt["help"] then
    usage
  end
rescue
  STDERR.puts $!
  exit 1
end

if $opt["auth"] then
  if $opt["auth"][0] == ?/ then
    load $opt["auth"]
  else
    load "#{File.dirname($0)}/#{$opt["auth"]}"
  end
end

main
