1# frozen_string_literal: false 2# 3# httpauth/htpasswd -- Apache compatible htpasswd file 4# 5# Author: IPR -- Internet Programming with Ruby -- writers 6# Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7# reserved. 8# 9# $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ 10 11require_relative 'userdb' 12require_relative 'basicauth' 13require 'tempfile' 14 15module WEBrick 16 module HTTPAuth 17 18 ## 19 # Htpasswd accesses apache-compatible password files. Passwords are 20 # matched to a realm where they are valid. For security, the path for a 21 # password database should be stored outside of the paths available to the 22 # HTTP server. 23 # 24 # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. 25 # 26 # To create an Htpasswd database with a single user: 27 # 28 # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' 29 # htpasswd.set_passwd 'my realm', 'username', 'password' 30 # htpasswd.flush 31 32 class Htpasswd 33 include UserDB 34 35 ## 36 # Open a password database at +path+ 37 38 def initialize(path, password_hash: nil) 39 @path = path 40 @mtime = Time.at(0) 41 @passwd = Hash.new 42 @auth_type = BasicAuth 43 @password_hash = password_hash 44 45 case @password_hash 46 when nil 47 # begin 48 # require "string/crypt" 49 # rescue LoadError 50 # warn("Unable to load string/crypt, proceeding with deprecated use of String#crypt, consider using password_hash: :bcrypt") 51 # end 52 @password_hash = :crypt 53 when :crypt 54 # require "string/crypt" 55 when :bcrypt 56 require "bcrypt" 57 else 58 raise ArgumentError, "only :crypt and :bcrypt are supported for password_hash keyword argument" 59 end 60 61 File.open(@path,"a").close unless File.exist?(@path) 62 reload 63 end 64 65 ## 66 # Reload passwords from the database 67 68 def reload 69 mtime = File::mtime(@path) 70 if mtime > @mtime 71 @passwd.clear 72 File.open(@path){|io| 73 while line = io.gets 74 line.chomp! 75 case line 76 when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z! 77 if @password_hash == :bcrypt 78 raise StandardError, ".htpasswd file contains crypt password, only bcrypt passwords supported" 79 end 80 user, pass = line.split(":") 81 when %r!\A[^:]+:\$2[aby]\$\d{2}\$.{53}\z! 82 if @password_hash == :crypt 83 raise StandardError, ".htpasswd file contains bcrypt password, only crypt passwords supported" 84 end 85 user, pass = line.split(":") 86 when /:\$/, /:{SHA}/ 87 raise NotImplementedError, 88 'MD5, SHA1 .htpasswd file not supported' 89 else 90 raise StandardError, 'bad .htpasswd file' 91 end 92 @passwd[user] = pass 93 end 94 } 95 @mtime = mtime 96 end 97 end 98 99 ## 100 # Flush the password database. If +output+ is given the database will 101 # be written there instead of to the original path. 102 103 def flush(output=nil) 104 output ||= @path 105 tmp = Tempfile.create("htpasswd", File::dirname(output)) 106 renamed = false 107 begin 108 each{|item| tmp.puts(item.join(":")) } 109 tmp.close 110 File::rename(tmp.path, output) 111 renamed = true 112 ensure 113 tmp.close 114 File.unlink(tmp.path) if !renamed 115 end 116 end 117 118 ## 119 # Retrieves a password from the database for +user+ in +realm+. If 120 # +reload_db+ is true the database will be reloaded first. 121 122 def get_passwd(realm, user, reload_db) 123 reload() if reload_db 124 @passwd[user] 125 end 126 127 ## 128 # Sets a password in the database for +user+ in +realm+ to +pass+. 129 130 def set_passwd(realm, user, pass) 131 if @password_hash == :bcrypt 132 # Cost of 5 to match Apache default, and because the 133 # bcrypt default of 10 will introduce significant delays 134 # for every request. 135 @passwd[user] = BCrypt::Password.create(pass, :cost=>5) 136 else 137 @passwd[user] = make_passwd(realm, user, pass) 138 end 139 end 140 141 ## 142 # Removes a password from the database for +user+ in +realm+. 143 144 def delete_passwd(realm, user) 145 @passwd.delete(user) 146 end 147 148 ## 149 # Iterate passwords in the database. 150 151 def each # :yields: [user, password] 152 @passwd.keys.sort.each{|user| 153 yield([user, @passwd[user]]) 154 } 155 end 156 end 157 end 158end 159