File: filesystem.rb

package info (click to toggle)
ruby-openid 2.1.8debian-6
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 1,676 kB
  • sloc: ruby: 16,506; xml: 219; sh: 24; makefile: 2
file content (271 lines) | stat: -rw-r--r-- 7,311 bytes parent folder | download | duplicates (3)
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