#!/usr/bin/ruby
#
# NAME
#  symbiosis-ftpd-check-password -- Check the an FTP user password
#
# SYNOPSIS
#  symbiosis-ftpd-check-password [ -h | --help ] [-m | --manual] [ -p | --prefix ]
#
# OPTIONS
#  -h, --help            Show a help message, and exit.
#
#  -m, --manual          Show this manual, and exit.
#
#  -p, --prefix          Specify the prefix directory (default /srv)
#
# USAGE
#
# This script is used by the Pure FTPd authentication daemon to check passwords
# in the Bytemark Symbiosis environment. It interfaces with the pure-authd.
#
# EXAMPLES
#
# To test, as a user other than root, do the following:
#
#   mkdir -p /srv/example.com/config
#   echo -n "foo" > /srv/example.com/config/ftp-password
#   AUTHD_ACCOUNT="example.com" AUTHD_PASSWORD="foo" symbiosis-ftpd-check-password
#
# It should return with an exit code of zero, and output similar to
#
#   auth_ok:1
#   uid:1000
#   gid:1000
#   dir:/srv/example.com/public/./
#   end
#
# SEE ALSO
#   pure-authd(8),  http://download.pureftpd.org/pub/pure-ftpd/doc/README.Authentication-Modules
#
# AUTHOR
#   Patrick J. Cherry <patrick@bytemark.co.uk>

require 'getoptlong'

manual = help = false
prefix = "/srv"

opts = GetoptLong.new(
         [ '--help',       '-h', GetoptLong::NO_ARGUMENT ],
         [ '--manual',     '-m', GetoptLong::NO_ARGUMENT ],
         [ '--prefix',     '-p', GetoptLong::OPTIONAL_ARGUMENT ]
       )

opts.each do |opt,arg|
  case opt
  when '--help'
    help = true
  when '--manual'
    manual = true
  when '--prefix'
    prefix = arg
  end
end

#
# CAUTION! Here be quality kode.
#
if manual or help
  # Open the file, stripping the shebang line
  lines = File.open(__FILE__){|fh| fh.readlines}[1..-1]

  found_synopsis = false

  lines.each do |line|

    line.chomp!
    break if line.empty?

    if help and !found_synopsis
      found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/)
      next
    end

    puts line[2..-1].to_s

    break if help and found_synopsis and line =~ /^#\s*$/

  end

  exit 0
end

require 'symbiosis/domains'
require 'symbiosis/domain/ftp'
require 'syslog'

# These are error codes suitable for dovecot.  Not sure why we're using them here!
TEMPORARY_ERROR = 111
PERMANENT_ERROR = 1
SUCCESS         = 0

ip       = "unknown"
service  = "ftp";
username = nil
ldomain  = nil
password = nil

ip       = ENV['AUTHD_REMOTE_IP' ] if ENV.has_key?('AUTHD_REMOTE_IP')
username = ENV['AUTHD_ACCOUNT'] if ENV.has_key?('AUTHD_ACCOUNT')
password = ENV['AUTHD_PASSWORD'] if ENV.has_key?('AUTHD_PASSWORD' )

# Open syslog
syslog = Syslog.open( File.basename($0), Syslog::LOG_NDELAY && Syslog::LOG_PERROR, Syslog::LOG_FTP )

begin
  #
  # username sanity check
  #
  if username.nil?
    syslog.info "No username given from #{ip} for #{service} service"
    syslog.err  "#{service} login failure from IP: #{ip} username: nil"

    print "auth_ok:0\nend\n"
    exit PERMANENT_ERROR
  end

  #
  # Work out if this ia multi or single user login
  #
  if username.include? "@" # new style ftp login
    (lusername,ldomain) = username.split("@",2)
    mode = :multi
  else
    ldomain = username
    mode = :single
  end

  domain = Symbiosis::Domains.find(ldomain, prefix)

  if domain.nil?
    syslog.info "Non-existent domain #{ldomain.inspect} from #{ip} for #{service} service"
    syslog.err  "#{service} login failure from IP: #{ip} username: #{username}"

    print "auth_ok:0\nend\n"
    exit PERMANENT_ERROR
  end

  case mode
    when :single
      user = domain.ftp_single_user
    when :multi
      user = domain.ftp_multi_users.find{|u| u.username == username}
    else
      syslog.info "Unknown FTP mode #{mode} for domain #{domain.name} from #{ip} for #{service} service"
      syslog.err  "#{service} login failure from IP: #{ip} username: #{username}"

      print "auth_ok:0\nend\n"
      exit PERMANENT_ERROR
  end

  if user.nil?
    syslog.info "Non-existent #{mode} user #{username.inspect} for domain #{domain.name} from #{ip} for #{service} service"
    syslog.err  "#{service} login failure from IP: #{ip} username: #{username}"

    print "auth_ok:0\nend\n"
    exit PERMANENT_ERROR
  end

  #
  # Try logging in.
  #
  unless user.login(password)
    syslog.info "Bad password for username #{username} from #{ip} for #{service} service"
    syslog.err  "#{service} login failure from IP: #{ip} username: #{username}"

    print "auth_ok:-1\nend\n"
    exit PERMANENT_ERROR
  end

  #
  # Work out results before printing.
  #
  results = [ "uid:#{user.uid}",
        "gid:#{user.gid}",
        "dir:#{user.chroot_dir}" ]

  results << "user_quota_size:#{user.quota}" unless user.quota.nil? or user.quota == 0

  #
  # Woo-hoo success!
  #
  print (["auth_ok:1"]+results+["end\n"]).join("\n")
  exit SUCCESS

rescue => err

  #
  # Rescue all exceptions, to make sure authentication doesn't happen
  # automatically when things fail
  #
  syslog.err "#{err.class}: #{err.to_s} for username #{username} from #{ip} for #{service} service"
  syslog.debug err.backtrace.join("\n")
  print "auth_ok:0\nend\n"
  exit TEMPORARY_ERROR

end

#
#  We should never get this far.
#
syslog.err "Something has gone badly wrong whilst authenticating username #{username}@#{ldomain} from #{ip} for #{service} service"
print "auth_ok:0\nend\n"
exit TEMPORARY_ERROR
