1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
|
require 'fileutils'
require 'pathname'
require 'tempfile'
require 'openid/util'
require 'openid/store/interface'
require 'openid/association'
module OpenID
module Store
class Filesystem < Interface
@@FILENAME_ALLOWED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-".split("")
# Create a Filesystem store instance, putting all data in +directory+.
def initialize(directory)
p_dir = Pathname.new(directory)
@nonce_dir = p_dir.join('nonces')
@association_dir = p_dir.join('associations')
@temp_dir = p_dir.join('temp')
self.ensure_dir(@nonce_dir)
self.ensure_dir(@association_dir)
self.ensure_dir(@temp_dir)
end
# Create a unique filename for a given server url and handle. The
# filename that is returned will contain the domain name from the
# server URL for ease of human inspection of the data dir.
def get_association_filename(server_url, handle)
unless server_url.index('://')
raise ArgumentError, "Bad server URL: #{server_url}"
end
proto, rest = server_url.split('://', 2)
domain = filename_escape(rest.split('/',2)[0])
url_hash = safe64(server_url)
if handle
handle_hash = safe64(handle)
else
handle_hash = ''
end
filename = [proto,domain,url_hash,handle_hash].join('-')
@association_dir.join(filename)
end
# Store an association in the assoc directory
def store_association(server_url, association)
assoc_s = association.serialize
filename = get_association_filename(server_url, association.handle)
f, tmp = mktemp
begin
begin
f.write(assoc_s)
f.fsync
ensure
f.close
end
begin
File.rename(tmp, filename)
rescue Errno::EEXIST
begin
File.unlink(filename)
rescue Errno::ENOENT
# do nothing
end
File.rename(tmp, filename)
end
rescue
self.remove_if_present(tmp)
raise
end
end
# Retrieve an association
def get_association(server_url, handle=nil)
# the filename with empty handle is the prefix for the associations
# for a given server url
filename = get_association_filename(server_url, handle)
if handle
return _get_association(filename)
end
assoc_filenames = Dir.glob(filename.to_s + '*')
assocs = assoc_filenames.collect do |f|
_get_association(f)
end
assocs = assocs.find_all { |a| not a.nil? }
assocs = assocs.sort_by { |a| a.issued }
return nil if assocs.empty?
return assocs[-1]
end
def _get_association(filename)
begin
assoc_file = File.open(filename, "r")
rescue Errno::ENOENT
return nil
else
begin
assoc_s = assoc_file.read
ensure
assoc_file.close
end
begin
association = Association.deserialize(assoc_s)
rescue
self.remove_if_present(filename)
return nil
end
# clean up expired associations
if association.expires_in == 0
self.remove_if_present(filename)
return nil
else
return association
end
end
end
# Remove an association if it exists, otherwise do nothing.
def remove_association(server_url, handle)
assoc = get_association(server_url, handle)
if assoc.nil?
return false
else
filename = get_association_filename(server_url, handle)
return self.remove_if_present(filename)
end
end
# Return whether the nonce is valid
def use_nonce(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > Nonce.skew
if server_url and !server_url.empty?
proto, rest = server_url.split('://',2)
else
proto, rest = '',''
end
raise "Bad server URL" unless proto && rest
domain = filename_escape(rest.split('/',2)[0])
url_hash = safe64(server_url)
salt_hash = safe64(salt)
nonce_fn = '%08x-%s-%s-%s-%s'%[timestamp, proto, domain, url_hash, salt_hash]
filename = @nonce_dir.join(nonce_fn)
begin
fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0200)
fd.close
return true
rescue Errno::EEXIST
return false
end
end
# Remove expired entries from the database. This is potentially expensive,
# so only run when it is acceptable to take time.
def cleanup
cleanup_associations
cleanup_nonces
end
def cleanup_associations
association_filenames = Dir[@association_dir.join("*").to_s]
count = 0
association_filenames.each do |af|
begin
f = File.open(af, 'r')
rescue Errno::ENOENT
next
else
begin
assoc_s = f.read
ensure
f.close
end
begin
association = OpenID::Association.deserialize(assoc_s)
rescue StandardError
self.remove_if_present(af)
next
else
if association.expires_in == 0
self.remove_if_present(af)
count += 1
end
end
end
end
return count
end
def cleanup_nonces
nonces = Dir[@nonce_dir.join("*").to_s]
now = Time.now.to_i
count = 0
nonces.each do |filename|
nonce = filename.split('/')[-1]
timestamp = nonce.split('-', 2)[0].to_i(16)
nonce_age = (timestamp - now).abs
if nonce_age > Nonce.skew
self.remove_if_present(filename)
count += 1
end
end
return count
end
protected
# Create a temporary file and return the File object and filename.
def mktemp
f = Tempfile.new('tmp', @temp_dir)
[f, f.path]
end
# create a safe filename from a url
def filename_escape(s)
s = '' if s.nil?
filename_chunks = []
s.split('').each do |c|
if @@FILENAME_ALLOWED.index(c)
filename_chunks << c
else
filename_chunks << sprintf("_%02X", c[0])
end
end
filename_chunks.join("")
end
def safe64(s)
s = OpenID::CryptUtil.sha1(s)
s = OpenID::Util.to_base64(s)
s.gsub!('+', '_')
s.gsub!('/', '.')
s.gsub!('=', '')
return s
end
# remove file if present in filesystem
def remove_if_present(filename)
begin
File.unlink(filename)
rescue Errno::ENOENT
return false
end
return true
end
# ensure that a path exists
def ensure_dir(dir_name)
FileUtils::mkdir_p(dir_name)
end
end
end
end
|