forget-passwords 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|