forget-passwords 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE +202 -0
- data/README.md +603 -0
- data/Rakefile +6 -0
- data/behaviour.org +112 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/content/basic-401.xhtml +15 -0
- data/content/basic-404.xhtml +10 -0
- data/content/basic-409.xhtml +14 -0
- data/content/basic-500.xhtml +10 -0
- data/content/cookie-expired.xhtml +15 -0
- data/content/email-409.xhtml +15 -0
- data/content/email-sent.xhtml +11 -0
- data/content/email.xhtml +10 -0
- data/content/logged-out-all.xhtml +10 -0
- data/content/logged-out.xhtml +10 -0
- data/content/nonce-expired.xhtml +15 -0
- data/content/not-on-list.xhtml +15 -0
- data/content/post-405.xhtml +10 -0
- data/content/uri-409.xhtml +10 -0
- data/etc/text-only.xsl +105 -0
- data/exe/forgetpw +7 -0
- data/forget-passwords.gemspec +67 -0
- data/lib/forget-passwords/cli.rb +514 -0
- data/lib/forget-passwords/fastcgi.rb +28 -0
- data/lib/forget-passwords/state.rb +535 -0
- data/lib/forget-passwords/template.rb +269 -0
- data/lib/forget-passwords/types.rb +118 -0
- data/lib/forget-passwords/version.rb +3 -0
- data/lib/forget-passwords.rb +635 -0
- metadata +312 -0
@@ -0,0 +1,514 @@
|
|
1
|
+
require 'commander'
|
2
|
+
require 'yaml'
|
3
|
+
require 'pathname'
|
4
|
+
require 'iso8601'
|
5
|
+
require 'uri'
|
6
|
+
require 'dry-schema'
|
7
|
+
require 'deep_merge'
|
8
|
+
|
9
|
+
require 'forget-passwords'
|
10
|
+
|
11
|
+
module ForgetPasswords
|
12
|
+
|
13
|
+
class CLI
|
14
|
+
include Commander::Methods
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
ONE_YEAR = ISO8601::Duration.new('P1Y').freeze
|
19
|
+
|
20
|
+
CFG_FILE = Pathname('forgetpw.yml').freeze
|
21
|
+
|
22
|
+
DEFAULTS = {
|
23
|
+
host: 'localhost',
|
24
|
+
port: 10101,
|
25
|
+
state: { dsn: 'sqlite://forgetpw.sqlite' },
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
Config = ForgetPasswords::App::Config.schema(
|
29
|
+
host: ForgetPasswords::Types::Hostname.default('localhost'.freeze),
|
30
|
+
port: Dry::Types['integer'].default(10101),
|
31
|
+
pid?: ForgetPasswords::Types::ExtantPathname).hash_default
|
32
|
+
|
33
|
+
def normalize_hash h, strings: false, flatten: false, dup: false,
|
34
|
+
freeze: false
|
35
|
+
return h unless h.is_a? Hash
|
36
|
+
out = {}
|
37
|
+
h.each do |k, v|
|
38
|
+
ks = k.to_s
|
39
|
+
ks = ks.to_sym unless strings
|
40
|
+
v = if v.is_a?(Hash)
|
41
|
+
normalize_hash v, strings: strings, flatten: flatten,
|
42
|
+
dup: dup, freeze: freeze
|
43
|
+
elsif v.respond_to?(:to_a)
|
44
|
+
v.to_a.map do |x|
|
45
|
+
normalize_hash x, strings: strings, flatten: flatten,
|
46
|
+
dup: dup, freeze: freeze
|
47
|
+
end
|
48
|
+
elsif flatten
|
49
|
+
v.is_a?(Numeric) ? v : v.to_s
|
50
|
+
else
|
51
|
+
v
|
52
|
+
end
|
53
|
+
v = v.dup if dup
|
54
|
+
v = v.freeze if freeze
|
55
|
+
out[ks] = v
|
56
|
+
end
|
57
|
+
out
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_config cfg = @cfgfile, clean: true, commit: true
|
61
|
+
unless cfg.is_a? Hash
|
62
|
+
raise 'need a config file' unless
|
63
|
+
cfg and (cfg.is_a?(Pathname) or cfg.respond_to?(:to_s))
|
64
|
+
cfg = Pathname(cfg).expand_path
|
65
|
+
if cfg.exist?
|
66
|
+
raise "Config file #{cfg} is not readable" unless cfg.readable?
|
67
|
+
cfg = YAML.load_file cfg
|
68
|
+
else
|
69
|
+
cfg = {}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
cfg = normalize_hash cfg if clean
|
74
|
+
|
75
|
+
merge_config @config, cfg, commit: true if commit
|
76
|
+
|
77
|
+
cfg
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_config cfg = @config
|
81
|
+
Config.call cfg
|
82
|
+
end
|
83
|
+
|
84
|
+
OPTION_MAP = {
|
85
|
+
base_url: :base,
|
86
|
+
query_key: :query,
|
87
|
+
cookie_key: :cookie,
|
88
|
+
lifetime: :expiry,
|
89
|
+
listen: :host,
|
90
|
+
}.freeze
|
91
|
+
|
92
|
+
def cmdline_config options, mapping = {}
|
93
|
+
options ||= {}
|
94
|
+
# Commander::Command::Options is weird
|
95
|
+
begin
|
96
|
+
h = options.__hash__.dup
|
97
|
+
rescue NoMethodError
|
98
|
+
h = options
|
99
|
+
end
|
100
|
+
|
101
|
+
out = {}
|
102
|
+
|
103
|
+
h.keys.each do |k|
|
104
|
+
next unless v = mapping[k]
|
105
|
+
v = [v] unless v.is_a? Array
|
106
|
+
x = out
|
107
|
+
v.each_index do |i|
|
108
|
+
vi = v[i]
|
109
|
+
if i + 1 == v.length
|
110
|
+
x[vi] = h[k] # assign the value to the hash member
|
111
|
+
else
|
112
|
+
x = x[vi] ||= {} # append another hash
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
out
|
118
|
+
end
|
119
|
+
|
120
|
+
def merge_config *cfg, commit: false, validate: false
|
121
|
+
raise 'wah wah need a config' unless (out = cfg.shift)
|
122
|
+
# normalize does an implicit deep clone
|
123
|
+
out = normalize_hash(out, dup: true)
|
124
|
+
|
125
|
+
until cfg.empty?
|
126
|
+
#out.deep_merge normalize_hash(cfg.shift, dup: true)
|
127
|
+
out = normalize_hash(cfg.shift, dup: true).deep_merge out
|
128
|
+
end
|
129
|
+
|
130
|
+
if validate
|
131
|
+
test = Config.call out
|
132
|
+
if test.success?
|
133
|
+
out = test.to_h
|
134
|
+
else
|
135
|
+
raise RuntimeError.new test.errors.messages
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
@config = out if commit
|
140
|
+
|
141
|
+
out
|
142
|
+
end
|
143
|
+
|
144
|
+
def write_config cfg = @config, file: @cfgfile
|
145
|
+
out = normalize_hash cfg, strings: true, flatten: true
|
146
|
+
file.open(?w) { |fh| fh.write out.to_yaml }
|
147
|
+
end
|
148
|
+
|
149
|
+
def connect dsn = @dsn
|
150
|
+
@state = State.connect dsn
|
151
|
+
end
|
152
|
+
|
153
|
+
public
|
154
|
+
|
155
|
+
def run
|
156
|
+
program :name, File.basename($0)
|
157
|
+
program :version, ForgetPasswords::VERSION
|
158
|
+
program :description, 'Command line manager for ForgetPasswords'
|
159
|
+
program :int_message, 'mkay bye'
|
160
|
+
|
161
|
+
@cfgfile = CFG_FILE.expand_path
|
162
|
+
@config = DEFAULTS.dup
|
163
|
+
|
164
|
+
global_option '-b', '--base-uri URI',
|
165
|
+
'A base URI for relative references' do |o|
|
166
|
+
@config[:base] = URI(o)
|
167
|
+
end
|
168
|
+
|
169
|
+
global_option '-c', '--config FILE',
|
170
|
+
'The location of the configuration file' do |o|
|
171
|
+
@cfgfile = Pathname(o).expand_path
|
172
|
+
end
|
173
|
+
|
174
|
+
global_option '-d', '--dsn STRING',
|
175
|
+
'Specify a data source name, overriding configuration' do |o|
|
176
|
+
@config[:state][:dsn] = o
|
177
|
+
end
|
178
|
+
|
179
|
+
global_option '-D', '--debug-sql',
|
180
|
+
'Log SQL queries to standard error' do
|
181
|
+
@log_sql = true
|
182
|
+
end
|
183
|
+
|
184
|
+
command :init do |c|
|
185
|
+
c.syntax = "#{program :name} init [OPTIONS]"
|
186
|
+
c.summary = 'Initializes configuration file and state database.'
|
187
|
+
c.description = <<-DESC
|
188
|
+
This command initializes the configuration file (default `$PWD/forgetpw.yml`)
|
189
|
+
and state database (default `sqlite://forgetpw.sqlite`). Global parameters
|
190
|
+
(-b, -c, -d) will be used to record the default base URL, config file
|
191
|
+
location, and data source name, respectively.
|
192
|
+
|
193
|
+
Most configuration parameters have sane defaults, and the base URI is
|
194
|
+
optional. If an existing configuration file is found at the specified
|
195
|
+
location, you will be asked before overwriting it. Initialization will
|
196
|
+
likewise overwrite any existing database tables, so you'll also get a
|
197
|
+
chance to move those out of the way if you want to keep them.
|
198
|
+
|
199
|
+
If you are using a network-attached RDBMS (Postgres, MySQL, Oracle,
|
200
|
+
etc), you will almost certainly need to create the designated
|
201
|
+
database, user, and any applicable access rights before running this
|
202
|
+
command.
|
203
|
+
DESC
|
204
|
+
|
205
|
+
c.option '--query-key TOKEN', 'A URI query key; defaults to `knock`'
|
206
|
+
c.option '--cookie-key TOKEN', 'A cookie key; defaults to `forgetpw`'
|
207
|
+
c.option '--expiry DURATION',
|
208
|
+
'Global default expiry, given as an ISO8601 duration (default P1Y)'
|
209
|
+
c.option '--url-expiry DURATION',
|
210
|
+
'Set the default expiry duration for URLs only'
|
211
|
+
c.option '--cookie-expiry DURATION',
|
212
|
+
'Set the default expiry duration for cookies only'
|
213
|
+
c.option '--user-var TOKEN',
|
214
|
+
'Environment variable name for user (default `FCGI_USER`)'
|
215
|
+
c.option '--redirect-var TOKEN',
|
216
|
+
'Environment variable name for redirect (default `FCGI_REDIRECT`)'
|
217
|
+
c.option '-l', '--listen HOST',
|
218
|
+
'Specify listening address for FastCGI daemon (default localhost)'
|
219
|
+
c.option '-p', '--port NUMBER',
|
220
|
+
'Specify TCP port for FastCGI daemon (default 10101)'
|
221
|
+
c.option '-P', '--pid FILE',
|
222
|
+
'Create a PID file when FastCGI daemon is detached'
|
223
|
+
|
224
|
+
c.action do |_, opts|
|
225
|
+
# check the directory where we're going to drop the config file
|
226
|
+
dir = @cfgfile.dirname
|
227
|
+
unless dir.exist?
|
228
|
+
rel = dir.relative_path_from Pathname.pwd
|
229
|
+
if agree "Directory #{rel} doesn't exist. Try to create it?"
|
230
|
+
begin
|
231
|
+
dir.mkpath
|
232
|
+
rescue Errno::EACCES
|
233
|
+
say "Could not create #{rel}. :("
|
234
|
+
exit 1
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# check for an existing config file
|
240
|
+
if @cfgfile.exist?
|
241
|
+
rel = @cfgfile.relative_path_from Pathname.pwd
|
242
|
+
# get confirmation if config file already exists
|
243
|
+
x = "Configuration file #{rel} already exists. Overwrite?"
|
244
|
+
unless agree x
|
245
|
+
say "Not overwriting #{rel}."
|
246
|
+
exit 1
|
247
|
+
end
|
248
|
+
|
249
|
+
# complain if not writable
|
250
|
+
unless @cfgfile.writable?
|
251
|
+
say "Not overwriting #{rel}, which is not writable."
|
252
|
+
exit 1
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# wrap these calls
|
257
|
+
begin
|
258
|
+
cfg = cmdline_config opts
|
259
|
+
merge_config @config, cfg, commit: true
|
260
|
+
write_config
|
261
|
+
rescue SystemCallError => e
|
262
|
+
rel = @cfgfile.relative_path_from Pathname.pwd
|
263
|
+
say "Could not write #{rel}: #{e}"
|
264
|
+
exit 1
|
265
|
+
#rescue OptionParser::InvalidArgument => e
|
266
|
+
# say "One or more of the command-line options was invalid: #{e}"
|
267
|
+
# exit 1
|
268
|
+
end
|
269
|
+
|
270
|
+
do_db = true
|
271
|
+
begin
|
272
|
+
state = State.new @config[:dsn], create: false, debug: @log_sql
|
273
|
+
|
274
|
+
# check for existence of database
|
275
|
+
if state.initialized?
|
276
|
+
x = "Database #{@config[:dsn]} is already initialized. Overwrite?"
|
277
|
+
unless agree x
|
278
|
+
say "Not overwriting #{@config[:dsn]}."
|
279
|
+
do_db = false
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# now create the tables
|
284
|
+
state.initialize! if do_db
|
285
|
+
|
286
|
+
rescue Sequel::DatabaseConnectionError => e
|
287
|
+
# complain if database doesn't exist or if i don't have access
|
288
|
+
say "Could not connect to #{@config[:dsn]}: #{e}"
|
289
|
+
do_db = false
|
290
|
+
end
|
291
|
+
|
292
|
+
# now tell the user what i did
|
293
|
+
say 'Created new configuration file' +
|
294
|
+
"#{do_db ? ' and state database' : ''}."
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
command :privilege do |c|
|
299
|
+
c.syntax = "#{program :name} privilege [OPTIONS] EMAIL|DOMAIN ..."
|
300
|
+
c.summary = "Adds or revokes privileges to an e-mail address or domain."
|
301
|
+
c.description = <<-DESC
|
302
|
+
This command will either add or revoke access privileges to one or
|
303
|
+
more e-mail address or e-mail domain, associated with a Web domain.
|
304
|
+
DESC
|
305
|
+
|
306
|
+
c.option '-d', '--domain DOMAIN',
|
307
|
+
'Constrain to the given Web domain'
|
308
|
+
c.option '-l', '--list', 'List instead of acting'
|
309
|
+
c.option '-r', '--revoke', 'Revoke privileges instead of granting them'
|
310
|
+
|
311
|
+
c.action do |args, opts|
|
312
|
+
read_config
|
313
|
+
@config = Config.(@config)
|
314
|
+
|
315
|
+
db = @config[:state]
|
316
|
+
# warn db.inspect
|
317
|
+
|
318
|
+
domain = opts.domain ||= ''
|
319
|
+
|
320
|
+
if opts.list
|
321
|
+
else
|
322
|
+
method = opts.revoke ? :revoke : :permit
|
323
|
+
db.transaction do
|
324
|
+
args.each do |email|
|
325
|
+
db.acl.send method, domain, email
|
326
|
+
say "added #{email} to #{domain.empty? ? ?* : domain}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
command :mint do |c|
|
335
|
+
c.syntax = "#{program :name} [mint] [OPTIONS] USERID [URL]"
|
336
|
+
c.summary = 'Mints a new URL associated with the given user.'
|
337
|
+
c.description = <<-DESC
|
338
|
+
This command mints a new URL associated with the given user. If the
|
339
|
+
URL is omitted, the slug will be appended to the configured base
|
340
|
+
URL. If that is missing too, the command will just return the
|
341
|
+
generated token. If there is already an active token for this user, it
|
342
|
+
will be reused with the new URL, unless you pass -n or -x, which will
|
343
|
+
add a new
|
344
|
+
|
345
|
+
This command will create a new record for a given user ID if none is
|
346
|
+
present. You can include an optional email address (-e), or you can make
|
347
|
+
the user ID an email address, LDAP DN, Kerberos principal, or whatever
|
348
|
+
you want. The user ID, whatever it ends up as, is what will be showing
|
349
|
+
up verbatim in the `REMOTE_USER` field of downstream Web apps. Note
|
350
|
+
that this database only maps one kind of identifier to another, and is
|
351
|
+
not meant to be authoritative storage for user profiles.
|
352
|
+
DESC
|
353
|
+
|
354
|
+
c.option '-e', '--email EMAIL',
|
355
|
+
'Set the email address for the (new) user'
|
356
|
+
c.option '-l', '--lifetime DURATION',
|
357
|
+
'How long this URL will work, as an ISO8601 duration (default P1Y)'
|
358
|
+
c.option '-n', '--new',
|
359
|
+
'Force minting a new token even if the current one is still fresh'
|
360
|
+
c.option '-x', '--expire',
|
361
|
+
'Expire any active tokens in circulation (implies --new)'
|
362
|
+
c.option '-1', '--oneoff', 'The token will expire after the first ' +
|
363
|
+
'time it is used (implies --new).'
|
364
|
+
|
365
|
+
c.action do |args, opts|
|
366
|
+
read_config
|
367
|
+
merge_config @config, cmdline_config(opts),
|
368
|
+
commit: true
|
369
|
+
|
370
|
+
user, url = *(args.map(&:strip))
|
371
|
+
|
372
|
+
raise Commander::Runner::CommandError.new 'No user supplied' unless
|
373
|
+
user and user != ''
|
374
|
+
if url and url != ''
|
375
|
+
begin
|
376
|
+
if @config[:base]
|
377
|
+
url = @config[:base].merge url
|
378
|
+
else
|
379
|
+
url = URI(url)
|
380
|
+
end
|
381
|
+
|
382
|
+
scheme = url.scheme.to_s.downcase
|
383
|
+
|
384
|
+
if scheme.start_with? 'http'
|
385
|
+
say 'Unencrypted HTTP will be insecure,' +
|
386
|
+
"but I assume you know what you're doing" if
|
387
|
+
url.scheme == 'http'
|
388
|
+
else
|
389
|
+
say 'Gonna be hard doing Web authentication ' +
|
390
|
+
"against a non-Web URL, but you're the boss!"
|
391
|
+
end
|
392
|
+
rescue URI::InvalidURIError => e
|
393
|
+
raise Commander::Runner::CommandError.new e
|
394
|
+
end
|
395
|
+
else
|
396
|
+
url = @config[:base] ? @config[:base].dup : nil
|
397
|
+
end
|
398
|
+
|
399
|
+
# handle implications of expire/oneoff options
|
400
|
+
opts.default :new => !!(opts.expire || opts.oneoff)
|
401
|
+
|
402
|
+
begin
|
403
|
+
|
404
|
+
# connect to the database
|
405
|
+
state = State.new @config[:dsn], debug: @log_sql,
|
406
|
+
query_expires: @config[:expiry][:url],
|
407
|
+
cookie_expires: @config[:expiry][:cookie]
|
408
|
+
|
409
|
+
id = state.id_for user, create: true, email: opts.email
|
410
|
+
|
411
|
+
# obtain the latest live token for this principal
|
412
|
+
token = state.token_for id unless opts.new
|
413
|
+
|
414
|
+
# eh i don't like this logic but it was the least-bad
|
415
|
+
# option i could think of at the time
|
416
|
+
if !token || opts.new
|
417
|
+
if opts.expire
|
418
|
+
say "Expiring all live tokens for #{user}."
|
419
|
+
# burn all existing query-string tokens
|
420
|
+
state.expire_tokens_for id, cookie: false
|
421
|
+
|
422
|
+
# XXX do we put in a command-line switch for burning
|
423
|
+
# the cookies too?
|
424
|
+
end
|
425
|
+
# create new token
|
426
|
+
token = state.new_token id, oneoff: opts.oneoff,
|
427
|
+
expires: @config[:expiry][:query]
|
428
|
+
end
|
429
|
+
|
430
|
+
if url
|
431
|
+
if (query = url.query)
|
432
|
+
query = URI::decode_www_form query
|
433
|
+
else
|
434
|
+
query = []
|
435
|
+
end
|
436
|
+
|
437
|
+
# now add the key
|
438
|
+
query << [@config[:query], token]
|
439
|
+
|
440
|
+
url.query = URI::encode_www_form query
|
441
|
+
|
442
|
+
say "Here's the link to give to #{user} (and only #{user}): " +
|
443
|
+
url.to_s
|
444
|
+
else
|
445
|
+
say "No URL given, so here's your token: #{token}"
|
446
|
+
end
|
447
|
+
|
448
|
+
exit 0
|
449
|
+
|
450
|
+
rescue Sequel::DatabaseConnectionError => e
|
451
|
+
say "Could not connect to #{@config[:dsn]}: #{e}"
|
452
|
+
exit 1
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
command :fcgi do |c|
|
458
|
+
c.syntax = "#{program :name} fcgi [OPTIONS]"
|
459
|
+
c.summary = 'Runs the ForgetPasswords FastCGI authenticator.'
|
460
|
+
c.description = <<-DESC
|
461
|
+
This command fires up the ForgetPasswords FastCGI authenticator service. By
|
462
|
+
default it runs in the foreground, listening on localhost, TCP port
|
463
|
+
10101. All of these parameters of course can be changed, either in
|
464
|
+
configuration or on the command line. Of course this daemon is only
|
465
|
+
half the setup, the other half being done on the Web server, using
|
466
|
+
something like `mod_authnz_fcgi`.
|
467
|
+
DESC
|
468
|
+
c.option '-l', '--listen HOST',
|
469
|
+
'Specify listening address (default localhost)'
|
470
|
+
c.option '-p', '--port NUMBER', 'Specify TCP port (default 10101)'
|
471
|
+
c.option '-z', '--detach', 'Detach and daemonize the process'
|
472
|
+
c.option '-P', '--pid FILE', 'Create a PID file when detached'
|
473
|
+
|
474
|
+
c.action do |args, opts|
|
475
|
+
require 'forget-passwords'
|
476
|
+
require 'rack'
|
477
|
+
|
478
|
+
# booooo
|
479
|
+
require 'forget-passwords/fastcgi'
|
480
|
+
|
481
|
+
read_config
|
482
|
+
merge_config @config,
|
483
|
+
cmdline_config(opts, { listen: :host, port: :port }), commit: true
|
484
|
+
@config = Config.(@config)
|
485
|
+
|
486
|
+
appcfg = @config.slice(
|
487
|
+
:keys, :vars, :targets, :templates, :email
|
488
|
+
).merge({ debug: @log_sql })
|
489
|
+
|
490
|
+
say 'Running authenticator daemon on ' +
|
491
|
+
"fcgi://#{@config[:host]}:#{@config[:port]}/"
|
492
|
+
|
493
|
+
Rack::Server.start({
|
494
|
+
# app: ForgetPasswords::App.new(@config[:dsn], debug: @log_sql),
|
495
|
+
app: ForgetPasswords::App.new(@config[:state], **appcfg),
|
496
|
+
server: 'hacked-fcgi',
|
497
|
+
environment: 'none',
|
498
|
+
daemonize: opts.detach,
|
499
|
+
Host: @config[:host],
|
500
|
+
Port: @config[:port],
|
501
|
+
})
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
default_command :mint
|
506
|
+
|
507
|
+
run!
|
508
|
+
end
|
509
|
+
|
510
|
+
def self.run
|
511
|
+
new.run
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# XXX THIS IS A HACK MFERS
|
2
|
+
require 'rack/handler'
|
3
|
+
require 'rack/handler/fastcgi'
|
4
|
+
|
5
|
+
module ForgetPasswords
|
6
|
+
# XXX this unfortunate chunk of code exists because of
|
7
|
+
# https://bz.apache.org/bugzilla/show_bug.cgi?id=65984
|
8
|
+
class FastCGI < Rack::Handler::FastCGI
|
9
|
+
|
10
|
+
def self.send_headers(out, status, headers)
|
11
|
+
out.print "Status: #{status}\r\n"
|
12
|
+
headers.each do |k, vs|
|
13
|
+
vs.split("\n").each { |v| out.print "#{k}: #{v}\r\n" }
|
14
|
+
end
|
15
|
+
out.print "\r\n"
|
16
|
+
# we remove out.flush from the headers
|
17
|
+
# out.flush
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.send_body(out, body)
|
21
|
+
body.each { |part| out.print part }
|
22
|
+
# this one we keep and put it outside the loop
|
23
|
+
out.flush
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Rack::Handler.register 'hacked-fcgi', FastCGI
|
28
|
+
end
|