#!/usr/pkg/bin/ruby32

#
# dtcpauth, manipulate POP authorization DB
#

#
# Copyright (c) 2002-2013 Hajimu UMEMOTO <ume@mahoroba.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $Mahoroba: src/dtcp/dtcpauth.rb,v 1.6 2013/06/02 14:05:50 ume Exp $
#

require 'optparse'
require 'dbm'
require 'etc'

POPAUTHUID = 'pop'
POPAUTHDB = '/usr/pkg/etc/apop.auth'

#$debug = true

def usage
  $stderr.print "usage: #{File.basename($0)} [--init] [--list [user | ALL]] [--delete user] [--user user [password]]\n"
end

def errmsg(msg)
  $stderr.print "#{File.basename($0)}: #{msg}\n"
end

class PopAuthDb

  def each
    @f.each { |user, password|
      yield(user.chop, obscure(strip_null(password)))
    }
  end

  def user(user)
    key = user + "\0"
    if !@f.has_key?(key)
      errmsg("no entry for \"#{user}\" in POP authentication DB")
      return false
    end
    return obscure(strip_null(@f[key]))
  end

  def set(user, password)
    @f[user + "\0"] = obscure(password) + "\0\0"
  end

  def delete(user)
    key = user + "\0"
    if !@f.has_key?(key)
      errmsg("User '#{user}' not found in authentication database")
      return false
    end
    @f.delete(key)
    return true
  end

  def close
    @f.close
    @f = nil
    Process.euid = @origuid
  end

  def opend?
    if @f
      return true
    end
    return false
  end

  private

  def initialize(mode = nil)
    pw = Etc.getpwnam(POPAUTHUID)
    if !pw
      errmsg("no user named #{POPAUTHUID}")
      return
    end
    @origuid = Process.euid
    begin
      Process.euid = pw[2]
    rescue
      errmsg("no permission to asscess POP authentication DB")
      return
    end
    @f = DBM.open(POPAUTHDB, mode)
    if !@f
      if mode
	errmsg("no password database found")
      end
      Process.euid = @origuid
      return
    end
  end

  def strip_null(p)
    return p.sub(/\0+$/, '')
  end

  def obscure(p)
    q = []
    p.each_byte { |byte|
      q.push(byte ^ 0xff)
    }
    return q.pack('C*')
  end

end

def getpassword(prompt = "Password")
  password = ''
  while true
    open('/dev/tty', 'r') { |tty|
      system("stty -echo")
      $stderr.print "#{prompt}: "
      password = tty.readline.chop!
    }
    system("stty sane")
    $stderr.print "\n"
    if password.size > 4
      break
    end
    print "Please use a longer password.\n"
  end
  return password
end

def initdb
  popauthdb = PopAuthDb.new
  if popauthdb.opend?
    popauthdb.close
    errmsg("there is POP authentication DB already")
    return false
  end
  popauthdb = PopAuthDb.new(0600)
  if popauthdb.opend?
    return false
  end
  popauthdb.close
  return true
end

def listuser(popauthdb, user)
  if user == 'ALL'
    popauthdb.each { |u, p|
      if $debug
	print "#{u.ljust(16)}: #{p}\n"
      else
	print "#{u.ljust(16)}: APOP\n"
      end
    }
  else
    p = popauthdb.user(user)
    if p
      if $debug
	print "#{user.ljust(16)}: #{p}\n"
      else
	print "#{user.ljust(16)}: APOP\n"
      end
    end
  end
  return true
end

def setuser(popauthdb, user, password)
  if !password
    print "Changing only APOP password for #{user}.\n"
    password = getpassword("New Password")
    password2 = getpassword("Retype new Password")
    if password != password2
      print "Mismatch -- password unchanged.\n"
      return false
    end
  end
  popauthdb.set(user, password)
  return true
end

begin
  params = ARGV.getopts('dilu', 'delete', 'init', 'list', 'user')
rescue
  usage()
  exit 1
end

params["delete"] = true if params["d"]
params["init"] = true if params["i"]
params["list"] = true if params["l"]
params["user"] = true if params["u"]

optnum = 0
[params["delete"], params["init"], params["list"], params["user"]].each { |opt|
  if opt
    optnum += 1
  end
}
if optnum != 1
  usage()
  exit 1
end

if params["init"]
  r = initdb()
else
  if ARGV.length < 1
    usage()
    exit 1
  end
  popauthdb = PopAuthDb.new
  if !popauthdb.opend?
    exit 1
  end
  if params["list"]
    r = listuser(popauthdb, ARGV[0])
  elsif params["user"]
    r = setuser(popauthdb, ARGV[0], ARGV[1])
  elsif params["delete"]
    r = popauthdb.delete(ARGV[0])
  end
  popauthdb.close
end
if !r
  exit 1
end
exit 0
