sbsm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/COPYING +515 -0
  2. data/History.txt +5 -0
  3. data/Manifest.txt +49 -0
  4. data/README.txt +41 -0
  5. data/Rakefile +28 -0
  6. data/data/_flavored_uri.grammar +24 -0
  7. data/data/_uri.grammar +22 -0
  8. data/data/_zone_uri.grammar +24 -0
  9. data/data/flavored_uri.grammar +24 -0
  10. data/data/uri.grammar +22 -0
  11. data/data/zone_uri.grammar +24 -0
  12. data/install.rb +1098 -0
  13. data/lib/cgi/drbsession.rb +37 -0
  14. data/lib/sbsm/cgi.rb +79 -0
  15. data/lib/sbsm/drb.rb +19 -0
  16. data/lib/sbsm/drbserver.rb +162 -0
  17. data/lib/sbsm/exception.rb +28 -0
  18. data/lib/sbsm/flavored_uri_parser.rb +47 -0
  19. data/lib/sbsm/index.rb +66 -0
  20. data/lib/sbsm/lookandfeel.rb +176 -0
  21. data/lib/sbsm/lookandfeelfactory.rb +50 -0
  22. data/lib/sbsm/lookandfeelwrapper.rb +109 -0
  23. data/lib/sbsm/redefine_19_cookie.rb +4 -0
  24. data/lib/sbsm/redirector.rb +37 -0
  25. data/lib/sbsm/request.rb +162 -0
  26. data/lib/sbsm/session.rb +542 -0
  27. data/lib/sbsm/state.rb +301 -0
  28. data/lib/sbsm/time.rb +29 -0
  29. data/lib/sbsm/trans_handler.rb +119 -0
  30. data/lib/sbsm/turing.rb +25 -0
  31. data/lib/sbsm/uri_parser.rb +45 -0
  32. data/lib/sbsm/user.rb +47 -0
  33. data/lib/sbsm/validator.rb +256 -0
  34. data/lib/sbsm/viralstate.rb +47 -0
  35. data/lib/sbsm/zone_uri_parser.rb +48 -0
  36. data/test/data/dos_file.txt +2 -0
  37. data/test/data/lnf_file.txt +2 -0
  38. data/test/data/mac_file.txt +1 -0
  39. data/test/stub/cgi.rb +35 -0
  40. data/test/suite.rb +29 -0
  41. data/test/test_drbserver.rb +83 -0
  42. data/test/test_index.rb +90 -0
  43. data/test/test_lookandfeel.rb +230 -0
  44. data/test/test_session.rb +372 -0
  45. data/test/test_state.rb +176 -0
  46. data/test/test_trans_handler.rb +447 -0
  47. data/test/test_user.rb +44 -0
  48. data/test/test_validator.rb +126 -0
  49. data/usage-en.txt +112 -0
  50. metadata +142 -0
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # State Based Session Management
4
+ # Copyright (C) 2004 Hannes Wyss
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License as published by the Free Software Foundation; either
9
+ # version 2.1 of the License, or (at your option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with this library; if not, write to the Free Software
18
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
+ #
20
+ # ywesee - intellectual capital connected, Winterthurerstrasse 52, CH-8006 Z�rich, Switzerland
21
+ # hwyss@ywesee.com
22
+ #
23
+ # Session -- sbsm -- 22.10.2002 -- hwyss@ywesee.com
24
+
25
+ require 'sbsm/cgi'
26
+ require 'sbsm/drb'
27
+ require 'sbsm/state'
28
+ require 'sbsm/lookandfeelfactory'
29
+ require 'sbsm/redefine_19_cookie'
30
+ require 'delegate'
31
+
32
+ module SBSM
33
+ class Session < SimpleDelegator
34
+ attr_reader :user, :active_thread, :app, :key, :cookie_input,
35
+ :unsafe_input, :valid_input, :request_path
36
+ include DRbUndumped
37
+ PERSISTENT_COOKIE_NAME = "sbsm-persistent-cookie"
38
+ DEFAULT_FLAVOR = nil
39
+ DEFAULT_LANGUAGE = nil
40
+ DEFAULT_STATE = State
41
+ DEFAULT_ZONE = nil
42
+ DRB_LOAD_LIMIT = 255 * 102400
43
+ EXPIRES = 60 * 60
44
+ LF_FACTORY = nil
45
+ LOOKANDFEEL = Lookandfeel
46
+ CAP_MAX_THRESHOLD = 8
47
+ MAX_STATES = 4
48
+ SERVER_NAME = nil
49
+ ARGV.push('') # satisfy cgi-offline prompt
50
+ @@cgi = CGI.new('html4')
51
+ def Session.reset_stats
52
+ @@stats = {}
53
+ end
54
+ reset_stats
55
+ @@stats_ptrn = /./
56
+ def Session.show_stats ptrn=@@stats_ptrn
57
+ if ptrn.is_a?(String)
58
+ ptrn = /#{ptrn}/i
59
+ end
60
+ puts sprintf("%8s %8s %8s %6s %10s Request-Path",
61
+ "Min", "Max", "Avg", "Num", "Total")
62
+ grand_total = requests = all_max = all_min = 0
63
+ @@stats.collect do |path, times|
64
+ total = times.inject do |a, b| a + b end
65
+ grand_total += total
66
+ size = times.size
67
+ requests += size
68
+ max = times.max
69
+ all_max = max > all_max ? max : all_max
70
+ min = times.min
71
+ all_min = min < all_min ? min : all_min
72
+ [min, max, total / size, size, total, path]
73
+ end.sort.each do |data|
74
+ line = sprintf("%8.2f %8.2f %8.2f %6i %10.2f %s", *data)
75
+ if ptrn.match(line)
76
+ puts line
77
+ end
78
+ end
79
+ puts sprintf("%8s %8s %8s %6s %10s Request-Path",
80
+ "Min", "Max", "Avg", "Num", "Total")
81
+ puts sprintf("%8.2f %8.2f %8.2f %6i %10.2f",
82
+ all_min, all_max,
83
+ requests > 0 ? grand_total / requests : 0,
84
+ requests, grand_total)
85
+ ''
86
+ end
87
+ def initialize(key, app, validator=nil)
88
+ touch()
89
+ reset_input()
90
+ reset_cookie()
91
+ @app = app
92
+ @key = key
93
+ @validator = validator
94
+ @attended_states = {}
95
+ @persistent_user_input = {}
96
+ logout()
97
+ @unknown_user_class = @user.class
98
+ @variables = {}
99
+ @mutex = Mutex.new
100
+ #ARGV.push('') # satisfy cgi-offline prompt
101
+ #@cgi = CGI.new('html4')
102
+ super(app)
103
+ end
104
+ def age(now=Time.now)
105
+ now - @mtime
106
+ end
107
+ def cap_max_states
108
+ if(@attended_states.size > self::class::CAP_MAX_THRESHOLD)
109
+ #puts "too many states in session! Keeping only #{self::class::MAX_STATES}"
110
+ #$stdout.flush
111
+ sorted = @attended_states.values.sort
112
+ sorted[0...(-self::class::MAX_STATES)].each { |state|
113
+ state.__checkout
114
+ @attended_states.delete(state.object_id)
115
+ }
116
+ @attended_states.size
117
+ end
118
+ end
119
+ def __checkout
120
+ @attended_states.each_value { |state| state.__checkout }
121
+ @attended_states.clear
122
+ flavor = @persistent_user_input[:flavor]
123
+ lang = @persistent_user_input[:language]
124
+ @persistent_user_input.clear
125
+ @persistent_user_input.store(:flavor, flavor)
126
+ @persistent_user_input.store(:language, lang)
127
+ @valid_input.clear
128
+ @unsafe_input.clear
129
+ @active_thread = nil
130
+ true
131
+ end
132
+ def cgi
133
+ @@cgi
134
+ end
135
+ @@msie_ptrn = /MSIE/
136
+ @@win_ptrn = /Win/i
137
+ def client_activex?
138
+ (ua = user_agent) && @@msie_ptrn.match(ua) && @@win_ptrn.match(ua)
139
+ end
140
+ @@nt5_ptrn = /Windows\s*NT\s*(\d+\.\d+)/i
141
+ def client_nt5?
142
+ (ua = user_agent) \
143
+ && (match = @@nt5_ptrn.match(user_agent)) \
144
+ && (match[1].to_f >= 5)
145
+ end
146
+ def cookie_set_or_get(key)
147
+ if(value = @valid_input[key])
148
+ set_cookie_input(key, value)
149
+ else
150
+ @cookie_input[key]
151
+ end
152
+ end
153
+ def get_cookie_input(key)
154
+ @cookie_input[key]
155
+ end
156
+ def cookie_name
157
+ self::class::PERSISTENT_COOKIE_NAME
158
+ end
159
+ def default_language
160
+ self::class::DEFAULT_LANGUAGE
161
+ end
162
+ def direct_event
163
+ @state.direct_event
164
+ end
165
+ def drb_process(request)
166
+ start = Time.now
167
+ html = @mutex.synchronize do
168
+ process(request)
169
+ to_html
170
+ end
171
+ (@@stats[@request_path] ||= []).push(Time.now - start)
172
+ html
173
+ end
174
+ def error(key)
175
+ @state.error(key) if @state.respond_to?(:error)
176
+ end
177
+ def errors
178
+ @state.errors.values if @state.respond_to?(:errors)
179
+ end
180
+ def error?
181
+ @state.error? if @state.respond_to?(:error?)
182
+ end
183
+ def event
184
+ @valid_input[:event]
185
+ end
186
+ def event_bound_user_input(key)
187
+ @event_user_input ||= {}
188
+ evt = state.direct_event
189
+ @event_user_input[evt] ||= {}
190
+ if(val = user_input(key))
191
+ @event_user_input[evt][key] = val
192
+ else
193
+ @event_user_input[evt][key]
194
+ end
195
+ end
196
+ def expired?(now=Time.now)
197
+ age(now) > EXPIRES
198
+ end
199
+ def force_login(user)
200
+ @user = user
201
+ end
202
+ def import_cookies(request)
203
+ reset_cookie()
204
+ if(cuki = request.cookies[self::class::PERSISTENT_COOKIE_NAME])
205
+ cuki.each { |cuki_str|
206
+ CGI.parse(CGI.unescape(cuki_str)).each { |key, val|
207
+ key = key.intern
208
+ valid = @validator.validate(key, val.compact.last)
209
+ @cookie_input.store(key, valid)
210
+ }
211
+ }
212
+ end
213
+ end
214
+ @@hash_ptrn = /([^\[]+)((\[[^\]]+\])+)/
215
+ @@index_ptrn = /[^\[\]]+/
216
+ def import_user_input(request)
217
+ # attempting to read the cgi-params more than once results in a
218
+ # DRbConnectionRefused Exception. Therefore, do it only once...
219
+ return if(@user_input_imported)
220
+ request.params.each { |key, value|
221
+ #puts "importing #{key} -> #{value}"
222
+ index = nil
223
+ @unsafe_input.push([key.to_s.dup, value.to_s.dup])
224
+ unless(key.nil? || key.empty?)
225
+ if match = @@hash_ptrn.match(key)
226
+ key = match[1]
227
+ index = match[2]
228
+ #puts key, index
229
+ end
230
+ key = key.intern
231
+ if(key == :confirm_pass)
232
+ pass = request.params["pass"]
233
+ #puts "pass:#{pass} - confirm:#{value}"
234
+ @valid_input[key] = @valid_input[:set_pass] \
235
+ = @validator.set_pass(pass, value)
236
+ else
237
+ valid = @validator.validate(key, value)
238
+ if(index)
239
+ target = (@valid_input[key] ||= {})
240
+ indices = []
241
+ index.scan(@@index_ptrn) { |idx|
242
+ indices.push idx
243
+ }
244
+ last = indices.pop
245
+ indices.each { |idx|
246
+ target = (target[idx] ||= {})
247
+ }
248
+ target.store(last, valid)
249
+ else
250
+ @valid_input[key] = valid
251
+ end
252
+ end
253
+ end
254
+ #puts "imported #{key} -> #{value} => #{@valid_input[key].inspect}"
255
+ }
256
+ @user_input_imported = true
257
+ #puts @unsafe_input.inspect
258
+ #puts @valid_input.inspect
259
+ #$stdout.flush
260
+ end
261
+ def infos
262
+ @state.infos if @state.respond_to?(:infos)
263
+ end
264
+ def info?
265
+ @state.info? if @state.respond_to?(:info?)
266
+ end
267
+ def is_crawler?
268
+ @is_crawler ||= if @request.respond_to?(:is_crawler?)
269
+ @request.is_crawler?
270
+ end
271
+ end
272
+ def language
273
+ cookie_set_or_get(:language) || default_language
274
+ end
275
+ def logged_in?
276
+ !@user.is_a?(@unknown_user_class)
277
+ end
278
+ def login
279
+ if(user = @app.login(self))
280
+ @user = user
281
+ end
282
+ end
283
+ def logout
284
+ __checkout
285
+ @user = @app.unknown_user()
286
+ @active_state = @state = self::class::DEFAULT_STATE.new(self, @user)
287
+ @state.init
288
+ @attended_states.store(@state.object_id, @state)
289
+ end
290
+ def lookandfeel
291
+ if(@lookandfeel.nil? \
292
+ || (@lookandfeel.flavor != flavor) \
293
+ || (@lookandfeel.language != persistent_user_input(:language)))
294
+ @lookandfeel = if self::class::LF_FACTORY
295
+ self::class::LF_FACTORY.create(self)
296
+ else
297
+ self::class::LOOKANDFEEL.new(self)
298
+ end
299
+ end
300
+ @lookandfeel
301
+ end
302
+ def flavor
303
+ @flavor ||= begin
304
+ user_input = persistent_user_input(:flavor)
305
+ user_input ||= @valid_input[:default_flavor]
306
+ lf_factory = self::class::LF_FACTORY
307
+ if(lf_factory && lf_factory.include?(user_input))
308
+ user_input
309
+ else
310
+ self::class::DEFAULT_FLAVOR
311
+ end
312
+ end
313
+ end
314
+ def http_headers
315
+ @state.http_headers
316
+ rescue DRb::DRbConnError
317
+ raise
318
+ rescue NameError, StandardError => err
319
+ puts "error in SBSM::Session#http_headers: #@request_path"
320
+ puts err.class, err.message
321
+ puts err.backtrace[0,5]
322
+ {'Content-Type' => 'text/plain'}
323
+ end
324
+ def http_protocol
325
+ @http_protocol ||= if(@request.respond_to?(:server_port) \
326
+ && @request.server_port == 443)
327
+ 'https'
328
+ else
329
+ 'http'
330
+ end
331
+ end
332
+ def input_keys
333
+ @valid_input.keys
334
+ end
335
+ def navigation
336
+ @user.navigation
337
+ end
338
+ def passthru(*args)
339
+ @request.passthru(*args)
340
+ end
341
+ def persistent_user_input(key)
342
+ if(value = user_input(key))
343
+ @persistent_user_input.store(key, value)
344
+ else
345
+ @persistent_user_input[key]
346
+ end
347
+ end
348
+ def process(request)
349
+ begin
350
+ @request = request
351
+ @request_method = request.request_method
352
+ @request_path = @request.unparsed_uri
353
+ @validator.reset_errors() if @validator
354
+ import_user_input(request)
355
+ import_cookies(request)
356
+ @state = active_state.trigger(event())
357
+ #FIXME: is there a better way to distinguish returning states?
358
+ # ... we could simply refuse to init if event == :sort, but that
359
+ # would not solve the problem cleanly, I think.
360
+ unless(@state.request_path)
361
+ @state.request_path = @request_path
362
+ @state.init
363
+ end
364
+ unless @state.volatile?
365
+ @active_state = @state
366
+ @attended_states.store(@state.object_id, @state)
367
+ end
368
+ @zone = @active_state.zone
369
+ @active_state.touch
370
+ cap_max_states
371
+ rescue DRb::DRbConnError
372
+ raise
373
+ rescue StandardError => err
374
+ #@state = @state.previous
375
+ puts "error in SBSM::Session#process: #@request_path"
376
+ puts err.class, err.message
377
+ puts err.backtrace[0,5]
378
+ $stdout.flush
379
+ ensure
380
+ @user_input_imported = false
381
+ end
382
+ ''
383
+ end
384
+ def reset
385
+ =begin
386
+ if(@active_thread \
387
+ && @active_thread.alive? \
388
+ && @active_thread != Thread.current)
389
+ begin
390
+ #@active_thread.exit
391
+ rescue StandardError
392
+ end
393
+ end
394
+ @active_thread = Thread.current
395
+ =end
396
+ if @redirected
397
+ @redirected = false
398
+ else
399
+ reset_input()
400
+ end
401
+ end
402
+ def reset_cookie
403
+ @cookie_input = {}
404
+ end
405
+ def reset_input
406
+ @valid_input = {}
407
+ @processing_errors = {}
408
+ @http_protocol = nil
409
+ @flavor = nil
410
+ @unsafe_input = []
411
+ end
412
+ def remote_addr
413
+ @remote_addr ||= if @request.respond_to?(:remote_addr)
414
+ @request.remote_addr
415
+ end
416
+ end
417
+ def remote_ip
418
+ @remote_ip ||= if(@request.respond_to?(:remote_host))
419
+ @request.remote_host
420
+ end
421
+ end
422
+ def set_cookie_input(key, val)
423
+ @cookie_input.store(key, val)
424
+ end
425
+ def server_name
426
+ @server_name ||= if @request.respond_to?(:server_name)
427
+ @request.server_name
428
+ else
429
+ self::class::SERVER_NAME
430
+ end
431
+ rescue DRb::DRbConnError
432
+ @server_name = self::class::SERVER_NAME
433
+ end
434
+ def state(event=nil)
435
+ @active_state
436
+ end
437
+ def touch
438
+ @mtime = Time.now
439
+ self
440
+ end
441
+ def to_html
442
+ @state.to_html(@@cgi)
443
+ rescue DRb::DRbConnError
444
+ raise
445
+ rescue StandardError => err
446
+ puts "error in SBSM::Session#to_html: #@request_path"
447
+ puts err.class, err.message
448
+ puts err.backtrace#[0,5]
449
+ $stdout.flush
450
+ [ err.class, err.message ].join("\n")
451
+ end
452
+ def user_agent
453
+ @user_agent ||= (@request.user_agent if @request.respond_to?(:user_agent))
454
+ end
455
+ @@input_ptrn = /([^\[]+)\[([^\]]+)\]/
456
+ def user_input(*keys)
457
+ if(keys.size == 1)
458
+ index = nil
459
+ key = keys.first.to_s
460
+ if match = @@input_ptrn.match(key)
461
+ key = match[1]
462
+ index = match[2]
463
+ end
464
+ key_sym = key.to_sym
465
+ valid = @valid_input[key_sym]
466
+ if(index && valid.respond_to?(:[]))
467
+ valid[index]
468
+ else
469
+ valid
470
+ end
471
+ else
472
+ keys.inject({}) { |inj, key|
473
+ inj.store(key, user_input(key))
474
+ inj
475
+ }
476
+ end
477
+ end
478
+ def valid_values(key)
479
+ vals = @validator.valid_values(key) unless @validator.nil?
480
+ vals || []
481
+ end
482
+ def warnings
483
+ @state.warnings if @state.respond_to?(:warnings)
484
+ end
485
+ def warning?
486
+ @state.warning? if @state.respond_to?(:warning?)
487
+ end
488
+ # CGI::SessionHandler compatibility
489
+ def restore
490
+ #puts "restore was called"
491
+ #@unix_socket = DRb.start_service('drbunix:', self)
492
+ hash = {
493
+ #:proxy => DRbObject.new(self, @unix_socket.uri)
494
+ :proxy => self,
495
+ }
496
+ hash.extend(DRbUndumped) # added for Ruby1.8 compliance
497
+ hash
498
+ end
499
+ def update
500
+ # nothing
501
+ end
502
+ def close
503
+ #@unix_socket.stop_service
504
+ # nothing
505
+ end
506
+ def delete
507
+ @app.delete_session @key
508
+ end
509
+ def zone
510
+ @valid_input[:zone] || @state.zone || self::class::DEFAULT_ZONE
511
+ end
512
+ def zones
513
+ @active_state.zones
514
+ end
515
+ def zone_navigation
516
+ @state.zone_navigation
517
+ end
518
+ def ==(other)
519
+ super
520
+ end
521
+ def <=>(other)
522
+ self.weighted_mtime <=> other.weighted_mtime
523
+ end
524
+ def [](key)
525
+ @variables[key]
526
+ end
527
+ def []=(key, val)
528
+ @variables[key] = val
529
+ end
530
+ private
531
+ def active_state
532
+ if(state_id = @valid_input[:state_id])
533
+ @attended_states[state_id]
534
+ end || @active_state
535
+ end
536
+ protected
537
+ attr_reader :mtime
538
+ def weighted_mtime
539
+ @mtime + @user.session_weight
540
+ end
541
+ end
542
+ end