rubycas-server 0.4.2 → 0.5.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.
@@ -13,26 +13,28 @@ module CASServer
13
13
  key_path = CASServer::Conf.ssl_key || CASServer::Conf.ssl_cert
14
14
  # look for the key in the ssl_cert if no ssl_key is specified
15
15
 
16
- raise "'#{cert_path}' is not a valid ssl certificate. Your 'ssl_cert' configuration" +
17
- " setting must be a path to a valid ssl certificate file." unless
18
- File.exists? cert_path
19
-
20
- raise "'#{key_path}' is not a valid ssl private key. Your 'ssl_key' configuration" +
21
- " setting must be a path to a valid ssl private key file." unless
22
- File.exists? key_path
23
-
24
- cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
25
- key = OpenSSL::PKey::RSA.new(File.read(key_path))
16
+ webrick_options = {:BindAddress => "0.0.0.0", :Port => CASServer::Conf.port}
17
+
18
+ unless cert_path.nil? && key_path.nil?
19
+ raise "'#{cert_path}' is not a valid ssl certificate. Your 'ssl_cert' configuration" +
20
+ " setting must be a path to a valid ssl certificate file." unless
21
+ File.exists? cert_path
22
+
23
+ raise "'#{key_path}' is not a valid ssl private key. Your 'ssl_key' configuration" +
24
+ " setting must be a path to a valid ssl private key file." unless
25
+ File.exists? key_path
26
+
27
+ cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
28
+ key = OpenSSL::PKey::RSA.new(File.read(key_path))
29
+
30
+ webrick_options[:SSLEnable] = true
31
+ webrick_options[:SSLVerifyClient] = ::OpenSSL::SSL::VERIFY_NONE
32
+ webrick_options[:SSLCertificate] = cert
33
+ webrick_options[:SSLPrivateKey] = key
34
+ end
26
35
 
27
36
  begin
28
- s = WEBrick::HTTPServer.new(
29
- :BindAddress => "0.0.0.0",
30
- :Port => CASServer::Conf.port,
31
- :SSLEnable => true,
32
- :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
33
- :SSLCertificate => cert,
34
- :SSLPrivateKey => key
35
- )
37
+ s = WEBrick::HTTPServer.new(webrick_options)
36
38
  rescue Errno::EACCES
37
39
  puts "\nThe server could not launch. Are you running on a privileged port? (e.g. port 443) If so, you must run the server as root."
38
40
  exit 2
@@ -41,7 +43,7 @@ module CASServer
41
43
  CASServer.create
42
44
  s.mount "#{CASServer::Conf.uri_path}", WEBrick::CampingHandler, CASServer
43
45
 
44
- puts "\n** CASServer is running at http://localhost:#{CASServer::Conf.port}#{CASServer::Conf.uri_path} and logging to '#{CASServer::Conf.log[:file]}'\n\n"
46
+ puts "\n** CASServer is running at http#{webrick_options[:SSLEnable] ? 's' : ''}://#{Socket.gethostname}:#{CASServer::Conf.port}#{CASServer::Conf.uri_path} and logging to '#{CASServer::Conf.log[:file]}'\n\n"
45
47
 
46
48
  # This lets Ctrl+C shut down your server
47
49
  trap(:INT) do
@@ -68,11 +70,6 @@ module CASServer
68
70
  require 'rubygems'
69
71
  require 'mongrel/camping'
70
72
 
71
- # camping has fixes for mongrel currently only availabe in SVN
72
- # ... you can install camping from svn (1.5.180) by running:
73
- # gem install camping --source code.whytheluckystiff.net
74
- gem 'camping', '~> 1.5.180'
75
-
76
73
  if $DAEMONIZE
77
74
  # check if log and pid are writable before daemonizing, otherwise we won't be able to notify
78
75
  # the user if we run into trouble later (since once daemonized, we can't write to stdout/stderr)
@@ -5,6 +5,22 @@ module CASServer
5
5
  "#{Time.now.to_i}r%X" % rand(10**32)
6
6
  end
7
7
  module_function :random_string
8
+
9
+ def log_controller_action(controller, params)
10
+ $LOG << "\n"
11
+
12
+ /`(.*)'/.match(caller[1])
13
+ method = $~[1]
14
+
15
+ if params.respond_to? :dup
16
+ params2 = params.dup
17
+ params2['password'] = '******' if params2['password']
18
+ else
19
+ params2 = params
20
+ end
21
+ $LOG.debug("Processing #{controller}::#{method} #{params2.inspect}")
22
+ end
23
+ module_function :log_controller_action
8
24
 
9
25
  class Logger < ::Logger
10
26
  def initialize(logdev, shift_age = 0, shift_size = 1048576)
@@ -1,9 +1,9 @@
1
1
  module CASServer
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 4
5
- TINY = 2
4
+ MINOR = 5
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
9
- end
9
+ end
@@ -8,7 +8,6 @@ Markaby::Builder.set(:indent, 2)
8
8
  module CASServer::Views
9
9
 
10
10
  def layout
11
-
12
11
  # wrap as XHTML only when auto_validation is on, otherwise pass right through
13
12
  if @use_layout
14
13
  xhtml_strict do
@@ -28,9 +27,10 @@ module CASServer::Views
28
27
 
29
28
 
30
29
  # 2.1.3
30
+ # The full login page.
31
31
  def login
32
32
  @use_layout = true
33
-
33
+
34
34
  table(:id => "login-box") do
35
35
  tr do
36
36
  td(:colspan => 2) do
@@ -52,47 +52,54 @@ module CASServer::Views
52
52
  img(:id => "logo", :src => "/themes/#{current_theme}/logo.png")
53
53
  end
54
54
  td(:id => "login-form-container") do
55
- form(:method => "post", :action => "/login", :id => "login-form",
56
- :onsubmit => "submit = document.getElementById('login-submit'); submit.value='Please wait...'; submit.disabled=true; return true;") do
57
- table(:id => "form-layout") do
58
- tr do
59
- td(:id => "username-label-container") do
60
- label(:id => "username-label", :for => "username") { "Username" }
61
- end
62
- td(:id => "username-container") do
63
- input(:type => "text", :id => "username", :name => "username",
64
- :size => "32", :tabindex => "1", :accesskey => "u")
65
- end
66
- end
67
- tr do
68
- td(:id => "password-label-container") do
69
- label(:id => "password-label", :for => "password") { "Password" }
70
- end
71
- td(:id => "password-container") do
72
- input(:type => "password", :id => "password", :name => "password",
73
- :size => "32", :tabindex => "2", :accesskey => "p", :autocomplete => "off")
74
- end
75
- end
76
- tr do
77
- td{}
78
- td(:id => "submit-container") do
79
- input(:type => "hidden", :id => "lt", :name => "lt", :value => @lt)
80
- input(:type => "hidden", :id => "service", :name => "service", :value => @service)
81
- input(:type => "hidden", :id => "warn", :name => "warn", :value => @warn)
82
- input(:type => "submit", :class => "button", :accesskey => "l", :value => "LOGIN", :tabindex => "4", :id => "login-submit")
83
- end
84
- end
85
- tr do
86
- td(:colspan => 2, :id => "infoline") { infoline }
87
- end
88
- end
55
+ @include_infoline = true
56
+ login_form
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Just the login form.
63
+ def login_form
64
+ form(:method => "post", :action => @form_action || '/login', :id => "login-form",
65
+ :onsubmit => "submit = document.getElementById('login-submit'); submit.value='Please wait...'; submit.disabled=true; return true;") do
66
+ table(:id => "form-layout") do
67
+ tr do
68
+ td(:id => "username-label-container") do
69
+ label(:id => "username-label", :for => "username") { "Username" }
70
+ end
71
+ td(:id => "username-container") do
72
+ input(:type => "text", :id => "username", :name => "username",
73
+ :size => "32", :tabindex => "1", :accesskey => "u")
74
+ end
75
+ end
76
+ tr do
77
+ td(:id => "password-label-container") do
78
+ label(:id => "password-label", :for => "password") { "Password" }
79
+ end
80
+ td(:id => "password-container") do
81
+ input(:type => "password", :id => "password", :name => "password",
82
+ :size => "32", :tabindex => "2", :accesskey => "p", :autocomplete => "off")
89
83
  end
90
84
  end
85
+ tr do
86
+ td{}
87
+ td(:id => "submit-container") do
88
+ input(:type => "hidden", :id => "lt", :name => "lt", :value => @lt)
89
+ input(:type => "hidden", :id => "service", :name => "service", :value => @service)
90
+ input(:type => "hidden", :id => "warn", :name => "warn", :value => @warn)
91
+ input(:type => "submit", :class => "button", :accesskey => "l", :value => "LOGIN", :tabindex => "4", :id => "login-submit")
92
+ end
93
+ end
94
+ tr do
95
+ td(:colspan => 2, :id => "infoline") { infoline }
96
+ end if @include_infoline
91
97
  end
92
98
  end
93
99
  end
94
100
 
95
101
  # 2.4.2
102
+ # CAS 1.0 validate response.
96
103
  def validate
97
104
  if @success
98
105
  text "yes\n#{@username}\n"
@@ -102,6 +109,7 @@ module CASServer::Views
102
109
  end
103
110
 
104
111
  # 2.5.2
112
+ # CAS 2.0 service validate response.
105
113
  def service_validate
106
114
  if @success
107
115
  tag!("cas:serviceResponse", 'xmlns:cas' => "http://www.yale.edu/tp/cas") do
@@ -120,6 +128,7 @@ module CASServer::Views
120
128
  end
121
129
 
122
130
  # 2.6.2
131
+ # CAS 2.0 proxy validate response.
123
132
  def proxy_validate
124
133
  if @success
125
134
  tag!("cas:serviceResponse", 'xmlns:cas' => "http://www.yale.edu/tp/cas") do
@@ -145,6 +154,7 @@ module CASServer::Views
145
154
  end
146
155
 
147
156
  # 2.7.2
157
+ # CAS 2.0 proxy request response.
148
158
  def proxy
149
159
  if @success
150
160
  tag!("cas:serviceResponse", 'xmlns:cas' => "http://www.yale.edu/tp/cas") do
@@ -183,3 +193,7 @@ module CASServer::Views
183
193
  end
184
194
  module_function :infoline
185
195
  end
196
+
197
+ if CASServer::Conf.custom_views_file
198
+ require CASServer::Conf.custom_views_file
199
+ end
data/lib/casserver.rb CHANGED
@@ -14,7 +14,9 @@ unless Object.method_defined? :gem
14
14
  alias gem require_gem
15
15
  end
16
16
 
17
- gem 'camping', '~> 1.5'
17
+
18
+ #gem 'camping', '~> 1.5.180'
19
+ $: << $CASSERVER_HOME + "/../vendor/camping-1.5.180/lib"
18
20
  require 'camping'
19
21
 
20
22
  require 'active_support'
@@ -64,7 +66,7 @@ CASServer.init_logger
64
66
  def CASServer.create
65
67
  CASServer::Models.create_schema
66
68
 
67
- $LOG.info("RubyCAS-Server initialized.")
69
+ $LOG.info("RubyCAS-Server #{CASServer::VERSION::STRING} initialized.")
68
70
 
69
71
  $LOG.debug("Configuration is:\n#{$CONF.to_yaml}")
70
72
  $LOG.debug("Authenticator is: #{$AUTH}")
@@ -88,6 +90,11 @@ if __FILE__ == $0 || $RUN
88
90
  $LOG.warn("Unable to create a pid file. You must use mongrel or webrick for this feature.")
89
91
  end
90
92
 
93
+ require 'casserver/version'
94
+ puts
95
+ puts "*** Starting RubyCAS-Server #{CASServer::VERSION::STRING} using codebase at #{$CASSERVER_HOME}"
96
+
97
+
91
98
  begin
92
99
  raise NoMethodError if CASServer::Conf.server.nil?
93
100
  send(CASServer::Conf.server)
data/lib/themes/cas.css CHANGED
@@ -3,6 +3,7 @@
3
3
  }
4
4
 
5
5
  body {
6
+ text-align: center; /* hack for IE */
6
7
  }
7
8
 
8
9
  label {
@@ -0,0 +1,78 @@
1
+ class MissingLibrary < Exception #:nodoc: all
2
+ end
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError => e
6
+ raise MissingLibrary, "ActiveRecord could not be loaded (is it installed?): #{e.message}"
7
+ end
8
+
9
+ $AR_EXTRAS = %{
10
+ Base = ActiveRecord::Base unless const_defined? :Base
11
+
12
+ def Y; ActiveRecord::Base.verify_active_connections!; self; end
13
+
14
+ class SchemaInfo < Base
15
+ end
16
+
17
+ def self.V(n)
18
+ @final = [n, @final.to_i].max
19
+ m = (@migrations ||= [])
20
+ Class.new(ActiveRecord::Migration) do
21
+ meta_def(:version) { n }
22
+ meta_def(:inherited) { |k| m << k }
23
+ end
24
+ end
25
+
26
+ def self.create_schema(opts = {})
27
+ opts[:assume] ||= 0
28
+ opts[:version] ||= @final
29
+ if @migrations
30
+ unless SchemaInfo.table_exists?
31
+ ActiveRecord::Schema.define do
32
+ create_table SchemaInfo.table_name do |t|
33
+ t.column :version, :float
34
+ end
35
+ end
36
+ end
37
+
38
+ si = SchemaInfo.find(:first) || SchemaInfo.new(:version => opts[:assume])
39
+ if si.version < opts[:version]
40
+ @migrations.each do |k|
41
+ k.migrate(:up) if si.version < k.version and k.version <= opts[:version]
42
+ k.migrate(:down) if si.version > k.version and k.version > opts[:version]
43
+ end
44
+ si.update_attributes(:version => opts[:version])
45
+ end
46
+ end
47
+ end
48
+ }
49
+
50
+ module Camping
51
+ module Models
52
+ A = ActiveRecord
53
+ # Base is an alias for ActiveRecord::Base. The big warning I'm going to give you
54
+ # about this: *Base overloads table_name_prefix.* This means that if you have a
55
+ # model class Blog::Models::Post, it's table name will be <tt>blog_posts</tt>.
56
+ #
57
+ # ActiveRecord is not loaded if you never reference this class. The minute you
58
+ # use the ActiveRecord or Camping::Models::Base class, then the ActiveRecord library
59
+ # is loaded.
60
+ Base = A::Base
61
+
62
+ # The default prefix for Camping model classes is the topmost module name lowercase
63
+ # and followed with an underscore.
64
+ #
65
+ # Tepee::Models::Page.table_name_prefix
66
+ # #=> "tepee_pages"
67
+ #
68
+ def Base.table_name_prefix
69
+ "#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
70
+ end
71
+ module_eval $AR_EXTRAS
72
+ end
73
+ end
74
+ Camping::S.sub! "autoload:Base,'camping/db'", ""
75
+ Camping::S.sub! "def Y;self;end", $AR_EXTRAS
76
+ Camping::Apps.each do |app|
77
+ app::Models.module_eval $AR_EXTRAS
78
+ end
@@ -0,0 +1,244 @@
1
+ # == About camping/fastcgi.rb
2
+ #
3
+ # Camping works very well with FastCGI, since your application is only loaded
4
+ # once -- when FastCGI starts. In addition, this class lets you mount several
5
+ # Camping apps under a single FastCGI process, to help save memory costs.
6
+ #
7
+ # So where do you use the Camping::FastCGI class? Use it in your application's
8
+ # postamble and then you can point your web server directly at your application.
9
+ # See Camping::FastCGI docs for more.
10
+ require 'camping'
11
+ require 'fcgi'
12
+
13
+ module Camping
14
+ # Camping::FastCGI is a small class for hooking one or more Camping apps up to
15
+ # FastCGI. Generally, you'll use this class in your application's postamble.
16
+ #
17
+ # == The Smallest Example
18
+ #
19
+ # if __FILE__ == $0
20
+ # require 'camping/fastcgi'
21
+ # Camping::FastCGI.start(YourApp)
22
+ # end
23
+ #
24
+ # This example is stripped down to the basics. The postamble has no database
25
+ # connection. It just loads this class and calls Camping::FastCGI.start.
26
+ #
27
+ # Now, in Lighttpd or Apache, you can point to your app's file, which will
28
+ # be executed, only to discover that your app now speaks the FastCGI protocol.
29
+ #
30
+ # Here's a sample lighttpd.conf (tested with Lighttpd 1.4.11) to serve as example:
31
+ #
32
+ # server.port = 3044
33
+ # server.bind = "127.0.0.1"
34
+ # server.modules = ( "mod_fastcgi" )
35
+ # server.document-root = "/var/www/camping/blog/"
36
+ # server.errorlog = "/var/www/camping/blog/error.log"
37
+ #
38
+ # #### fastcgi module
39
+ # fastcgi.server = ( "/" => (
40
+ # "localhost" => (
41
+ # "socket" => "/tmp/camping-blog.socket",
42
+ # "bin-path" => "/var/www/camping/blog/blog.rb",
43
+ # "check-local" => "disable",
44
+ # "max-procs" => 1 ) ) )
45
+ #
46
+ # The file <tt>/var/www/camping/blog/blog.rb</tt> is the Camping app with
47
+ # the postamble.
48
+ #
49
+ # == Mounting Many Apps
50
+ #
51
+ # require 'camping/fastcgi'
52
+ # fast = Camping::FastCGI.new
53
+ # fast.mount("/blog", Blog)
54
+ # fast.mount("/tepee", Tepee)
55
+ # fast.mount("/", Index)
56
+ # fast.start
57
+ #
58
+ class FastCGI
59
+ CHUNK_SIZE=(4 * 1024)
60
+
61
+ attr_reader :mounts
62
+
63
+ # Creates a Camping::FastCGI class with empty mounts.
64
+ def initialize
65
+ @mounts = {}
66
+ end
67
+ # Mounts a Camping application. The +dir+ being the name of the directory
68
+ # to serve as the application's root. The +app+ is a Camping class.
69
+ def mount(dir, app)
70
+ dir.gsub!(/\/{2,}/, '/')
71
+ dir.gsub!(/\/+$/, '')
72
+ @mounts[dir] = app
73
+ end
74
+
75
+ #
76
+ # Starts the FastCGI main loop.
77
+ def start(&blk)
78
+ FCGI.each do |req|
79
+ camp_do(req, &blk)
80
+ end
81
+ end
82
+
83
+ # A simple single-app starter mechanism
84
+ #
85
+ # Camping::FastCGI.start(Blog)
86
+ #
87
+ def self.start(app)
88
+ cf = Camping::FastCGI.new
89
+ cf.mount("/", app)
90
+ cf.start
91
+ end
92
+
93
+ # Serve an entire directory of Camping apps. (See
94
+ # http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.)
95
+ #
96
+ # Use this method inside your FastCGI dispatcher:
97
+ #
98
+ # #!/usr/local/bin/ruby
99
+ # require 'rubygems'
100
+ # require 'camping/fastcgi'
101
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => "/path/to/db"
102
+ # Camping::FastCGI.serve("/home/why/cvs/camping/examples")
103
+ #
104
+ def self.serve(path, index=nil)
105
+ require 'camping/reloader'
106
+ if File.directory? path
107
+ fast = Camping::FastCGI.new
108
+ script_load = proc do |script|
109
+ app = Camping::Reloader.new(script)
110
+ fast.mount("/#{app.mount}", app)
111
+ app
112
+ end
113
+ Dir[File.join(path, '*.rb')].each &script_load
114
+ fast.mount("/", index) if index
115
+
116
+ fast.start do |dir, app|
117
+ Dir[File.join(path, dir, '*.rb')].each do |script|
118
+ smount = "/" + File.basename(script, '.rb')
119
+ script_load[script] unless fast.mounts.has_key? smount
120
+ end
121
+ end
122
+ else
123
+ start(Camping::Reloader.new(path))
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def camp_do(req)
130
+ root, path, dir, app = "/"
131
+ if ENV['FORCE_ROOT'] and ENV['FORCE_ROOT'].to_i == 1
132
+ path = req.env['SCRIPT_NAME']
133
+ else
134
+ root = req.env['SCRIPT_NAME']
135
+ path = req.env['PATH_INFO']
136
+ end
137
+
138
+ dir, app = @mounts.max { |a,b| match(path, a[0]) <=> match(path, b[0]) }
139
+ unless dir and app
140
+ dir, app = '/', Camping
141
+ end
142
+ yield dir, app if block_given?
143
+
144
+ req.env['SERVER_SCRIPT_NAME'] = req.env['SCRIPT_NAME']
145
+ req.env['SERVER_PATH_INFO'] = req.env['PATH_INFO']
146
+ req.env['SCRIPT_NAME'] = File.join(root, dir)
147
+ req.env['PATH_INFO'] = path.gsub(/^#{dir}/, '')
148
+
149
+ controller = app.run(SeekStream.new(req.in), req.env)
150
+ sendfile = nil
151
+ headers = {}
152
+ controller.headers.each do |k, v|
153
+ if k =~ /^X-SENDFILE$/i and !ENV['SERVER_X_SENDFILE']
154
+ sendfile = v
155
+ else
156
+ headers[k] = v
157
+ end
158
+ end
159
+
160
+ body = controller.body
161
+ controller.body = ""
162
+ controller.headers = headers
163
+
164
+ req.out << controller.to_s
165
+ if sendfile
166
+ File.open(sendfile, "rb") do |f|
167
+ while chunk = f.read(CHUNK_SIZE) and chunk.length > 0
168
+ req.out << chunk
169
+ end
170
+ end
171
+ elsif body.respond_to? :read
172
+ while chunk = body.read(CHUNK_SIZE) and chunk.length > 0
173
+ req.out << chunk
174
+ end
175
+ body.close if body.respond_to? :close
176
+ else
177
+ req.out << body.to_s
178
+ end
179
+ rescue Exception => e
180
+ req.out << server_error(root, path, exc, req)
181
+ ensure
182
+ req.finish
183
+ end
184
+
185
+ def server_error(root, path, exc, req)
186
+ "Content-Type: text/html\r\n\r\n" +
187
+ "<h1>Camping Problem!</h1>" +
188
+ "<h2><strong>#{root}</strong>#{path}</h2>" +
189
+ "<h3>#{exc.class} #{esc exc.message}</h3>" +
190
+ "<ul>" + exc.backtrace.map { |bt| "<li>#{esc bt}</li>" }.join + "</ul>" +
191
+ "<hr /><p>#{req.env.inspect}</p>"
192
+ end
193
+
194
+ def match(path, mount)
195
+ m = path.match(/^#{Regexp::quote mount}(\/|$)/)
196
+ if m; m.end(0)
197
+ else -1
198
+ end
199
+ end
200
+
201
+ def esc(str)
202
+ str.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
203
+ end
204
+
205
+ class SeekStream
206
+ def initialize(stream)
207
+ @last_read = ""
208
+ @stream = stream
209
+ @buffer = ""
210
+ end
211
+ def eof?
212
+ @buffer.empty? && @stream.eof?
213
+ end
214
+ def each
215
+ while true
216
+ pull(1024) until eof? or @buffer.index("\n")
217
+ return nil if eof?
218
+ yield @buffer.slice!(0..(@buffer.index("\n") || -1))
219
+ end
220
+ end
221
+ def pull(len)
222
+ @buffer += @stream.read(len).to_s
223
+ end
224
+ def read(len = 16384)
225
+ pull(len)
226
+ @last_read =
227
+ if eof?
228
+ nil
229
+ else
230
+ @buffer.slice!(0...len)
231
+ end
232
+ end
233
+ def seek(len, typ)
234
+ raise NotImplementedError, "only IO::SEEK_CUR is supported with SeekStream" if typ != IO::SEEK_CUR
235
+ raise NotImplementedError, "only rewinding is supported with SeekStream" if len > 0
236
+ raise NotImplementedError, "rewinding #{-len} past the buffer #{@last_read.size} start not supported with SeekStream" if -len > @last_read.size
237
+ @buffer = @last_read[len..-1] + @buffer
238
+ @last_read = ""
239
+ self
240
+ end
241
+ end
242
+
243
+ end
244
+ end