#!/usr/bin/ruby -w # # Copyright (c) 2002 Mark Longair. # # $Id: ruby-sample,v 1.2 2002/03/20 13:26:58 mark Exp $ # # This script is an example authenticator for tpop3d. It has not been # heavily tested; the intention is to provide a skeleton which you # might want to use to write real Ruby-based authenticators. # # Authentication is based on the contents of /etc/tpop3d.users. Lines # of that file are dealt with in the following way: # # - If the line is empty or entirely whitespace, it is ignored. # # - If the first non whitespace character of the line is # then the # line is ignored. # # - If the line is of the form :, the two # parts constitute a valid username / password pair. # # There's no APOP support at the moment, because Digest::MD5 is only # distributed with Ruby 1.7, which isn't released yet. In view of # that, adding MD5 hashing just for this script doesn't seem # worthwhile... # # For more details on auth_other authenticators, see tpop3d.conf(5) # #----------------------------------------------------------------------- # The mail spools are accessed by the user and group specified here: spool_user = "vmail" spool_group = "vmail" # You can alter the location of the password file with this: $password_file = "/etc/tpop3d.users" # mailbox_dir has //user appended to it to find the mail spool # of an authenticated user. If you make mailbox_dir nil, then the # normal mechanism (via configuration directives mailbox: and # auth-other-mailbox:) is used to find the mailbox. mailbox_dir = "/var/spool/vmail" # mailbox_dir = nil # There is no logging in this authenticator, but if you need to add # some calls to log_message for debugging purposes, then the output # will go to this log file... $authenticator_log = "/tmp/sample-authenticator.log" #----------------------------------------------------------------------- # A usage message, to remind people what the program is for... def usage print <\0\0 If is empty # (i.e. the first character read is null) the method returns # immediately. If at any point end of file is reached, the method # raises EOFError def each_pair loop do key = "" value = "" # Get key... loop do c = getc if ! c raise EOFError end if c == 0 if key.empty? # ... and we haven't read any characters yet return else break # That's the end of the key. end end key << c end # Get value... loop do c = getc if ! c raise EOFError end if c == 0 # Then we've found a complete pair. (n.b. value is allowed # to be empty...) yield key, value break end value << c end end end end # An exception we will need later... class AuthFailure < RuntimeError end # Look up a user, domain and password combination in the password # file. Returns true if the combination is in the file, and nil # otherwise. Can throw exceptions if the password file is missing, # etc. etc. def authenticates_correctly? ( user, domain, password ) open( $password_file, "r" ) do |f| f.each do |line| line.chomp! next if line =~ '^\s*$' next if line =~ '^\s*#' if line =~ '^\s*([\w\.\-]+)@([\w\.\-]+)\s*:\s*([\w\.\-]+)\s*$' file_user = $1 file_domain = $2 file_password = $3 if (user == file_user) and (domain == file_domain) and (password == file_password) return TRUE end else raise AuthFailure.new "A line in the users file is badly formed: `#{line}'" end end return nil end end #----------------------------------------------------------------------- # The main body of the code... # Largely pointless argument parsing; in here because you might # plausibly want to add some options to allow testing... require "getoptlong" options = GetoptLong.new( [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ) begin options.each do |opt, arg| case opt when "--help" usage exit end end rescue print "Unknown command line option.\n" usage exit end loop do begin # Now parse the authentication request... method = nil timestamp = nil user = nil password = nil digest = nil clienthost = nil local_part = nil domain = nil $stdin.each_pair do |key, value| case key when "method" method = value when "timestamp" timestamp = value when "user" user = value when "pass" password = value when "digest" digest = value when "clienthost" clienthost = value when "local_part" local_part = value when "domain" domain = value else raise AuthFailure.new( "Unknown key `#{key}' in pair: (#{key},#{value})" ) end end raise AuthFailure.new( "No method specified" ) unless method raise AuthFailure.new( "No user specified" ) unless user raise AuthFailure.new( "No local_part specified" ) unless user raise AuthFailure.new( "No domain specified" ) unless user if method == "APOP" unless timestamp raise AuthFailure.new( "APOP method was chosen, but no timestamp was provided." ) end unless digest raise AuthFailure.new( "APOP method was chosen, but no digest was provided." ) end unless password raise AuthFailure.new( "APOP method was chosen, but a password was provided." ) end raise AuthFailure.new( "APOP is not currently supported by this authenticator." ) elsif method == "PASS" if timestamp raise AuthFailure.new "PASS method was chosen, but a timestamp was provided" end if digest raise AuthFailure.new "PASS method was chosen, but a digest was provided" end if authenticates_correctly?( local_part, domain, password ) $stdout.write_pair "result", "YES" $stdout.write_pair "logmsg", "Authentication succeeded for user #{user}" $stdout.write_pair "uid", spool_user $stdout.write_pair "gid", spool_group $stdout.write_pair "domain", domain if mailbox_dir $stdout.write_pair "mailbox", "#{mailbox_dir}/#{domain}/#{local_part}" end $stdout.put_zero $stdout.flush next else raise AuthFailure.new "Authentication failed for user #{user}" end elsif method == "ONLOGIN" # We don't do anything with the ONLOGIN information, so return an # empty packet. $stdout.put_zero $stdout.flush else raise AuthFailure.new "Method was `#{method}' - should be `PASS', `APOP' or `ONLOGIN'" end rescue AuthFailure => message $stdout.write_pair "result", "NO" $stdout.write_pair "logmsg", message $stdout.put_zero $stdout.flush rescue EOFError # We hit EOF unexpectely, so just exit... exit rescue $stdout.write_pair "result", "NO" $stdout.write_pair "logmsg", "There was an error during authentication: " + $! $stdout.put_zero $stdout.flush end end