1#
2# httpauth/htdigest.rb -- Apache compatible htdigest file
3#
4# Author: IPR -- Internet Programming with Ruby -- writers
5# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
6# reserved.
7#
8# $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $
9
10require 'webrick/httpauth/userdb'
11require 'webrick/httpauth/digestauth'
12require 'tempfile'
13
14module WEBrick
15  module HTTPAuth
16
17    ##
18    # Htdigest accesses apache-compatible digest password files.  Passwords are
19    # matched to a realm where they are valid.  For security, the path for a
20    # digest password database should be stored outside of the paths available
21    # to the HTTP server.
22    #
23    # Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and
24    # stores passwords using cryptographic hashes.
25    #
26    #   htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
27    #   htpasswd.set_passwd 'my realm', 'username', 'password'
28    #   htpasswd.flush
29
30    class Htdigest
31      include UserDB
32
33      ##
34      # Open a digest password database at +path+
35
36      def initialize(path)
37        @path = path
38        @mtime = Time.at(0)
39        @digest = Hash.new
40        @mutex = Mutex::new
41        @auth_type = DigestAuth
42        open(@path,"a").close unless File::exist?(@path)
43        reload
44      end
45
46      ##
47      # Reloads passwords from the database
48
49      def reload
50        mtime = File::mtime(@path)
51        if mtime > @mtime
52          @digest.clear
53          open(@path){|io|
54            while line = io.gets
55              line.chomp!
56              user, realm, pass = line.split(/:/, 3)
57              unless @digest[realm]
58                @digest[realm] = Hash.new
59              end
60              @digest[realm][user] = pass
61            end
62          }
63          @mtime = mtime
64        end
65      end
66
67      ##
68      # Flush the password database.  If +output+ is given the database will
69      # be written there instead of to the original path.
70
71      def flush(output=nil)
72        output ||= @path
73        tmp = Tempfile.new("htpasswd", File::dirname(output))
74        begin
75          each{|item| tmp.puts(item.join(":")) }
76          tmp.close
77          File::rename(tmp.path, output)
78        rescue
79          tmp.close(true)
80        end
81      end
82
83      ##
84      # Retrieves a password from the database for +user+ in +realm+.  If
85      # +reload_db+ is true the database will be reloaded first.
86
87      def get_passwd(realm, user, reload_db)
88        reload() if reload_db
89        if hash = @digest[realm]
90          hash[user]
91        end
92      end
93
94      ##
95      # Sets a password in the database for +user+ in +realm+ to +pass+.
96
97      def set_passwd(realm, user, pass)
98        @mutex.synchronize{
99          unless @digest[realm]
100            @digest[realm] = Hash.new
101          end
102          @digest[realm][user] = make_passwd(realm, user, pass)
103        }
104      end
105
106      ##
107      # Removes a password from the database for +user+ in +realm+.
108
109      def delete_passwd(realm, user)
110        if hash = @digest[realm]
111          hash.delete(user)
112        end
113      end
114
115      ##
116      # Iterate passwords in the database.
117
118      def each # :yields: [user, realm, password_hash]
119        @digest.keys.sort.each{|realm|
120          hash = @digest[realm]
121          hash.keys.sort.each{|user|
122            yield([user, realm, hash[user]])
123          }
124        }
125      end
126    end
127  end
128end
129