sbsm 1.0.0

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.
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