1#
2# httpauth/htpasswd -- Apache compatible htpasswd 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: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $
9
10require 'webrick/httpauth/userdb'
11require 'webrick/httpauth/basicauth'
12require 'tempfile'
13
14module WEBrick
15  module HTTPAuth
16
17    ##
18    # Htpasswd accesses apache-compatible password files.  Passwords are
19    # matched to a realm where they are valid.  For security, the path for a
20    # password database should be stored outside of the paths available to the
21    # HTTP server.
22    #
23    # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth.
24    #
25    # To create an Htpasswd database with a single user:
26    #
27    #   htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file'
28    #   htpasswd.set_passwd 'my realm', 'username', 'password'
29    #   htpasswd.flush
30
31    class Htpasswd
32      include UserDB
33
34      ##
35      # Open a password database at +path+
36
37      def initialize(path)
38        @path = path
39        @mtime = Time.at(0)
40        @passwd = Hash.new
41        @auth_type = BasicAuth
42        open(@path,"a").close unless File::exist?(@path)
43        reload
44      end
45
46      ##
47      # Reload passwords from the database
48
49      def reload
50        mtime = File::mtime(@path)
51        if mtime > @mtime
52          @passwd.clear
53          open(@path){|io|
54            while line = io.gets
55              line.chomp!
56              case line
57              when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z!
58                user, pass = line.split(":")
59              when /:\$/, /:{SHA}/
60                raise NotImplementedError,
61                      'MD5, SHA1 .htpasswd file not supported'
62              else
63                raise StandardError, 'bad .htpasswd file'
64              end
65              @passwd[user] = pass
66            end
67          }
68          @mtime = mtime
69        end
70      end
71
72      ##
73      # Flush the password database.  If +output+ is given the database will
74      # be written there instead of to the original path.
75
76      def flush(output=nil)
77        output ||= @path
78        tmp = Tempfile.new("htpasswd", File::dirname(output))
79        begin
80          each{|item| tmp.puts(item.join(":")) }
81          tmp.close
82          File::rename(tmp.path, output)
83        rescue
84          tmp.close(true)
85        end
86      end
87
88      ##
89      # Retrieves a password from the database for +user+ in +realm+.  If
90      # +reload_db+ is true the database will be reloaded first.
91
92      def get_passwd(realm, user, reload_db)
93        reload() if reload_db
94        @passwd[user]
95      end
96
97      ##
98      # Sets a password in the database for +user+ in +realm+ to +pass+.
99
100      def set_passwd(realm, user, pass)
101        @passwd[user] = make_passwd(realm, user, pass)
102      end
103
104      ##
105      # Removes a password from the database for +user+ in +realm+.
106
107      def delete_passwd(realm, user)
108        @passwd.delete(user)
109      end
110
111      ##
112      # Iterate passwords in the database.
113
114      def each # :yields: [user, password]
115        @passwd.keys.sort.each{|user|
116          yield([user, @passwd[user]])
117        }
118      end
119    end
120  end
121end
122