kamu-wang 0.02

Sign up to get free protection for your applications and to get access to all the features.
data/lib/wang.rb ADDED
@@ -0,0 +1,437 @@
1
+ # vim: set noet:
2
+ #
3
+ # WANG - Web Access with No Grief
4
+ # http://github.com/kamu/wang/
5
+
6
+ require 'socket'
7
+ require 'uri'
8
+ require 'stringio'
9
+ require 'zlib'
10
+ require 'logger'
11
+ require 'yaml'
12
+ require 'timeout'
13
+ require 'base64'
14
+
15
+ class URI::Generic
16
+ def to_uri
17
+ self
18
+ end
19
+ end
20
+
21
+ class String
22
+ def to_uri
23
+ URI.parse(self)
24
+ end
25
+ end
26
+
27
+ module WANG
28
+ Response = Struct.new(:method, :uri, :status, :headers)
29
+
30
+ DEFAULT_OPEN_TIMEOUT = 60
31
+ DEFAULT_READ_TIMEOUT = 60
32
+ INFINITE_REDIR_COUNT = 7
33
+
34
+ # Creates a new instance of WANG::Client
35
+ #
36
+ # For more info, check WANG::Client.new
37
+ def self.new *args
38
+ Client.new(*args)
39
+ end
40
+
41
+ class TCPSocket < TCPSocket # add the timeouts :nodoc:
42
+ def initialize *args # allows passing of the timeout values
43
+ custom_args = args.shift
44
+ @read_timeout = custom_args[:read_timeout]
45
+ open_timeout = custom_args[:open_timeout]
46
+ Timeout::timeout(open_timeout) { super }
47
+ end
48
+
49
+ TIMEOUT_READ = %w{read readpartial gets readline}
50
+ TIMEOUT_READ.each {|m|
51
+ class_eval "def #{m}(*args); Timeout::timeout(@read_timeout) { super }; end;"
52
+ }
53
+ end
54
+
55
+ class Client
56
+ attr_accessor :responses
57
+
58
+ # Creates a new instance of WANG::Client
59
+ #
60
+ # Accepts a hash containing named arguments. Arguments:
61
+ # [:read_timeout] defines the timeout for socket reading in seconds
62
+ # [:open_timeout] defines the timeout for connecting in seconds
63
+ # [:debug] any value passed defines debug mode
64
+ # [:proxy_address] defines the proxy address to use for all the requests made (host:port)
65
+ def initialize args = {}
66
+ @log = Logger.new(STDOUT)
67
+ @log.level = args[:debug] ? Logger::DEBUG : Logger::WARN
68
+
69
+ @jar = Jar.new
70
+ @socket = nil
71
+ @host, @port = nil, nil # create a struct that combines host & port?
72
+ @responses = []
73
+ @read_timeout = args[:read_timeout] || DEFAULT_READ_TIMEOUT
74
+ @open_timeout = args[:open_timeout] || DEFAULT_OPEN_TIMEOUT
75
+ @proxy_host, @proxy_port = args[:proxy_address] ? args[:proxy_address].split(':', 2) : [nil, nil]
76
+
77
+ @log.debug("Connecting through a proxy: #{@proxy_host}:#{@proxy_port}") if @proxy_host
78
+ @log.debug("Using #{@read_timeout} as the read timeout and #{@open_timeout} as the open timeout")
79
+ end
80
+
81
+ # Issues a HEAD request.
82
+ #
83
+ # Returns +nil+ for the body.
84
+ def head url, referer = nil
85
+ @log.debug("HEAD: #{url.to_s}")
86
+ request('HEAD', url.to_uri, referer)
87
+ end
88
+
89
+ # Fetches a page using GET method
90
+ #
91
+ # If passed, referer will be sent to the server. Otherwise the last visited URL will be sent to the server as the referer.
92
+ def get url, referer = nil
93
+ @log.debug("GET: #{url.to_s}")
94
+ request("GET", url.to_uri, referer)
95
+ end
96
+
97
+ # Fetches a page using POST method
98
+ #
99
+ # Data can either be a String or a Hash. If passed a String, it will send it to the server as the POST data. If passed a Hash, it will be converted to post data and correctly escaped.
100
+ #
101
+ # If passed, referer will be sent to the server. Otherwise the last visited URL will be sent to the server as the referer.
102
+ def post url, data, referer = nil
103
+ @log.debug("POST: #{url.to_s}")
104
+ request("POST", url.to_uri, referer, data)
105
+ end
106
+
107
+ # Issues a PUT request. See post for more details.
108
+ def put url, data, referer = nil
109
+ @log.debug("PUT: #{url.to_s}")
110
+ request("PUT", url.to_uri, referer, data)
111
+ end
112
+
113
+ # Issues a DELETE request.
114
+ #
115
+ # Returns +nil+ for the body.
116
+ def delete url, referer = nil
117
+ @log.debug("DELETE: #{url.to_s}")
118
+ request("DELETE", url.to_uri, referer)
119
+ end
120
+
121
+ # Saves cookie from this Client instance's Jar to the given io
122
+ def save_cookies io
123
+ @jar.save(io)
124
+ end
125
+
126
+ # Loads cookies to this Client instance's Jar from the given io
127
+ def load_cookies io
128
+ @jar.load(io)
129
+ end
130
+
131
+ # Sets the HTTP authentication username & password, which are then used for requests
132
+ # Call with +nil+ as username to remove authentication
133
+ def set_auth username, password=''
134
+ @http_auth = username ? Base64.encode64(username+':'+password).chomp : nil # for some reason, encode64 might add a \n
135
+ end
136
+
137
+ private
138
+ def request method, uri, referer = nil, data = nil
139
+ uri.path = "/" if uri.path.empty? # fix the path to contain / right here, otherwise it should be added to cookie stuff too
140
+ uri = @responses.last.uri + uri unless uri.is_a?(URI::HTTP) # if the uri is relative, combine it with the uri of the latest response
141
+ check_socket *(@proxy_host ? [@proxy_host, @proxy_port] : [uri.host, uri.port])
142
+
143
+ referer = referer || @responses.last.nil? ? nil : @responses.last.uri
144
+ @responses.clear if not @responses.empty? and not redirect?(@responses.last.status)
145
+
146
+ #http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3
147
+ #there's no defined redir count that's considered infinite, so try something that makes sense
148
+ if @responses.length > INFINITE_REDIR_COUNT
149
+ return #raise an error?
150
+ end
151
+
152
+ @socket << generate_request_headers(method, uri, referer)
153
+ @socket << "Authorization: Basic #{@http_auth}\r\n" if @http_auth
154
+
155
+ if @jar.has_cookies_for?(uri)
156
+ @socket << "Cookie: #{@jar.cookies_for(uri)}\r\n"
157
+ @log.debug("SENDING COOKIES: #{@jar.cookies_for(uri)}")
158
+ end
159
+
160
+ data = data.map {|k,v| "#{Utils.escape(k)}=#{Utils.escape(v)}"}.join("&") if data.is_a?(Hash)
161
+
162
+ if data
163
+ @socket << "Content-Type: application/x-www-form-urlencoded\r\n"
164
+ @socket << "Content-Length: #{data.length}\r\n"
165
+ end
166
+ @socket << "\r\n"
167
+ @socket << data if data
168
+
169
+ status = read_status
170
+ @log.debug("STATUS: #{status}")
171
+ headers = read_headers(uri)
172
+ @log.debug("HEADERS: #{headers.inspect}")
173
+ body = read_body(headers) if returns_body?(method)
174
+ @log.debug("WANGJAR: #{@jar.inspect}")
175
+
176
+ @socket.close if headers["connection"] =~ /close/
177
+
178
+ @responses << Response.new(method, uri, status, headers)
179
+ return follow_redirect(headers["location"]) if redirect?(status)
180
+
181
+ body &&= decompress(headers["content-encoding"], body)
182
+
183
+ return status, headers, body
184
+ end
185
+
186
+ def generate_request_headers request_method, uri, referer
187
+ request_path = uri.path + (uri.query.nil? ? '' : "?#{uri.query}")
188
+ request_host = uri.host + (uri.port ? ":#{uri.port}" : '')
189
+ [
190
+ "#{request_method} #{@proxy_host ? uri.to_s : request_path} HTTP/1.1",
191
+ "Host: #{request_host}",
192
+ "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12",
193
+ "Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
194
+ "Accept-Language: en-us,en;q=0.5",
195
+ "Accept-Encoding: gzip,deflate,identity",
196
+ "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7",
197
+ "Keep-Alive: 300",
198
+ "Connection: keep-alive",
199
+ referer.nil? ? "" : "Referer: #{referer}\r\n" # an extra \r\n is needed for the last entry
200
+ ].join("\r\n")
201
+ end
202
+
203
+ def read_status
204
+ line = @socket.gets("\n")
205
+ status = line.match(%r{^HTTP/1\.\d (\d+) })[1]
206
+ return status.to_i
207
+ end
208
+
209
+ def read_headers uri
210
+ headers = Hash.new
211
+ key = nil # allow using the latest key when appending multiline headers
212
+ while header = @socket.gets("\n")
213
+ header.chomp!
214
+ break if header.empty?
215
+ if header =~ /^\s/
216
+ headers[key] << header.strip
217
+ else
218
+ key, val = header.split(": ", 2)
219
+ if key =~ /^Set-Cookie2?$/i #do we dare consider set-cookie2 the same?
220
+ @jar.consume(val, uri)
221
+ else
222
+ headers.store(key.downcase, val)
223
+ end
224
+ end
225
+ end
226
+
227
+ return headers
228
+ end
229
+
230
+ def read_body headers
231
+ body = ""
232
+ if headers["transfer-encoding"] =~ /chunked/i # read chunked body
233
+ while true
234
+ line = @socket.readline
235
+ chunk_len = line.slice(/[0-9a-fA-F]+/).hex
236
+ break if chunk_len == 0
237
+ while chunk_len > 0 # make sure to read the whole chunk
238
+ buf = @socket.read(chunk_len)
239
+ chunk_len -= buf.length
240
+ body << buf
241
+ end
242
+ @socket.read 2 # read the damn linechange
243
+ end
244
+ until (line = @socket.gets) and (line.nil? or line.sub(/\r?\n?/, "").empty?); end # read the chunk footers and the last line
245
+ elsif headers["content-length"]
246
+ clen = headers["content-length"].to_i
247
+ while body.length < clen
248
+ body << @socket.read([clen - body.length, 4096].min)
249
+ end
250
+ else #fallback that'll just consume all the data available
251
+ begin
252
+ while true
253
+ body << @socket.readpartial(4096)
254
+ end
255
+ rescue EOFError
256
+ end
257
+ end
258
+
259
+ return body
260
+ end
261
+
262
+ # Given an HTTP status code, returns whether it signifies a redirect.
263
+ def redirect? status
264
+ status.to_i / 100 == 3
265
+ end
266
+
267
+ # Given an HTTP method, returns whether a body should be read.
268
+ def returns_body? request_method
269
+ not ['HEAD', 'DELETE'].include?(request_method)
270
+ end
271
+
272
+ def follow_redirect location
273
+ @log.debug(location.inspect)
274
+ dest = location.to_uri
275
+ get(dest)
276
+ end
277
+
278
+ def decompress type, body
279
+ case type
280
+ when "gzip"
281
+ return Zlib::GzipReader.new(StringIO.new(body)).read
282
+ when "deflate"
283
+ begin
284
+ return Zlib::Inflate.inflate(body)
285
+ rescue Zlib::DataError # check http://www.ruby-forum.com/topic/136825 for more info
286
+ return Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body)
287
+ end
288
+ else
289
+ return body
290
+ end
291
+ end
292
+
293
+ def check_socket host, port = 'http'
294
+ connect(host, port) if @socket.nil? or @socket.closed? or @host.nil? or not @host.eql? host or @port.nil? or not @port.eql? port
295
+ end
296
+
297
+ def connect host, port
298
+ @log.debug("Connecting to #{host}:#{port}")
299
+ @socket.close unless @socket.nil? or @socket.closed?
300
+ @socket = TCPSocket.new({:read_timeout => @read_timeout, :open_timeout => @open_timeout}, host, port)
301
+ @host = host
302
+ @port = port
303
+ end
304
+ end
305
+
306
+ class Cookie
307
+ attr_accessor :key, :value, :domain, :path, :expires
308
+
309
+ def initialize key = nil, value = nil
310
+ @key, @value = key, value
311
+ @domain, @path, @expires = nil
312
+ end
313
+
314
+ def parse raw_cookie, uri = nil
315
+ keyval, *attributes = raw_cookie.split(/;\s*/)
316
+ @key, @value = keyval.split("=", 2)
317
+
318
+ attributes.each do |at|
319
+ case at
320
+ when /domain=(.*)/i
321
+ @domain = $1
322
+ when /expires=(.*)/i
323
+ @expires = begin
324
+ Time.parse($1)
325
+ rescue
326
+ nil
327
+ end
328
+ when /path=(.*)/i
329
+ @path = $1
330
+ end
331
+ end
332
+
333
+ @domain = uri.host if @domain.nil? and uri
334
+ @path = "/" if @path.nil? and uri
335
+ @path.sub!(/\/$/, "") if @path #remove the trailing /, because path matching automatically adds it
336
+
337
+ self
338
+ end
339
+
340
+ def same? c
341
+ self.key.eql? c.key and self.domain.eql? c.domain and self.path.eql? c.path
342
+ end
343
+
344
+ def match? uri
345
+ match_domain?(uri.host) and match_path?(uri.path)
346
+ end
347
+
348
+ def expired?
349
+ @expires.is_a?(Time) ? @expires < Time.now : false
350
+ end
351
+
352
+ def match_domain? domain # TODO check if this fully follows the spec
353
+ case @domain
354
+ when /^\d+\.\d+\.\d+\.\d+$/ # ip address
355
+ domain.eql?(@domain)
356
+ when /^\./ # so domain = site.com and subdomains could match @domain to .site.com
357
+ domain =~ /#{Regexp.escape(@domain)}$/i
358
+ else
359
+ domain.downcase.eql?(@domain.downcase)
360
+ end
361
+ end
362
+
363
+ def match_path? path
364
+ path =~ /^#{Regexp.escape(@path)}(?:\/.*)?$/
365
+ end
366
+ end
367
+
368
+ class Jar
369
+ def initialize
370
+ @jar = []
371
+ end
372
+
373
+ def consume raw_cookie, uri = nil
374
+ cookie = Cookie.new.parse(raw_cookie, uri)
375
+ add(cookie)
376
+ end
377
+
378
+ def add c
379
+ i = index(c)
380
+ if i.nil?
381
+ @jar << c
382
+ else
383
+ @jar[i] = c
384
+ end
385
+ end
386
+
387
+ def has_cookies_for? uri
388
+ not cookies_for(uri).empty?
389
+ end
390
+
391
+ def cookies_for uri
392
+ @jar -= @jar.select { |c| c.expired? }
393
+ @jar.select { |c| c.match?(uri) }.map{ |c| "#{c.key}=#{c.value}" }.join("; ")
394
+ end
395
+
396
+ def index c
397
+ @jar.each do |cookie|
398
+ return @jar.index(cookie) if cookie.same? c
399
+ end
400
+
401
+ nil
402
+ end
403
+
404
+ def include? c
405
+ not index(c).nil?
406
+ end
407
+
408
+ def save io
409
+ io << @jar.to_yaml
410
+ io.close
411
+ end
412
+
413
+ def load io
414
+ saved_jar = YAML::load(io)
415
+
416
+ saved_jar.each do |c|
417
+ add(c)
418
+ end
419
+ end
420
+ end
421
+
422
+ module Utils
423
+ # URL-encode a string.
424
+ def self.escape string
425
+ string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
426
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
427
+ end.tr(' ', '+')
428
+ end
429
+
430
+ # URL-decode a string.
431
+ def self.unescape string
432
+ string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
433
+ [$1.delete('%')].pack('H*')
434
+ end
435
+ end
436
+ end
437
+ end
data/rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake/gempackagetask'
4
+ require 'rcov/rcovtask'
5
+
6
+ task :default => [:test]
7
+
8
+ spec = Gem::Specification.new do |s|
9
+ s.platform = Gem::Platform::RUBY
10
+ s.name = 'wang'
11
+ s.version = "0.01"
12
+ s.summary = "Web Access with No Grief."
13
+ s.authors = ["Kamu", "Joux3"]
14
+ s.email = "mr.kamu@gmail.com"
15
+ s.homepage = "http://github.com/kamu/wang/tree"
16
+ s.requirements << 'none'
17
+ s.require_path = 'lib'
18
+ s.files = FileList["rakefile", "lib/**/*", "test/**/*"]
19
+ s.test_files = FileList["test/wang_test.rb"]
20
+ end
21
+
22
+ Rake::TestTask.new do |t|
23
+ t.test_files = FileList["test/wang_test.rb"]
24
+ t.verbose = true
25
+ end
26
+
27
+ Rake::GemPackageTask.new(spec) do |p|
28
+ p.need_tar = true
29
+ end
30
+
31
+ Rcov::RcovTask.new do |t|
32
+ t.test_files = FileList["test/wang_test.rb"]
33
+ t.verbose = true
34
+ end
data/test/htdigest ADDED
@@ -0,0 +1 @@
1
+ tester:WANG digest HTTP auth test:45485e998d046e343f2b4550520a1ae6
data/test/wang_test.rb ADDED
@@ -0,0 +1,178 @@
1
+ # vim: set noet:
2
+ #
3
+ # WANG - Web Access with No Grief
4
+ # http://github.com/kamu/wang/
5
+
6
+ require 'test/unit'
7
+ require 'wang'
8
+ require 'test/wang_test_server'
9
+
10
+ $test_server = WANGTestServer.new
11
+ Thread.new do
12
+ $test_server.start
13
+ end
14
+
15
+ class WangTest < Test::Unit::TestCase
16
+ def setup
17
+ @client = WANG.new(:debug => true, :read_timeout=>0.9) # small read timeout shouldn't fail local tests
18
+ end
19
+
20
+ def test_returns_success
21
+ status, headers, body = @client.get('http://localhost:8080/')
22
+ assert_equal 200, status
23
+ end
24
+
25
+ def test_compression
26
+ status, headers, body = @client.get('http://localhost:8080/test_inflate')
27
+ assert_equal 'deflate', headers['content-encoding']
28
+ assert_equal 'inflate worked', body
29
+
30
+ status, headers, body = @client.get('http://localhost:8080/test_gzip')
31
+ assert_equal 'gzip', headers['content-encoding']
32
+ assert_equal 'gzip worked', body
33
+ end
34
+
35
+ def test_returns_headers_hash
36
+ status, headers, body = @client.get('http://localhost:8080/')
37
+ assert headers.is_a?(Hash)
38
+ assert_equal 'text/html', headers['content-type']
39
+ end
40
+
41
+ def test_supports_custom_ports
42
+ assert_nothing_raised { @client.get('http://localhost:8080/redirect') }
43
+ end
44
+
45
+ def test_follows_redirect
46
+ status, headers, body = @client.get('http://localhost:8080/redirect')
47
+ assert_equal 'http://localhost:8080/redirect', @client.responses.first.uri.to_s
48
+ assert_equal 'http://localhost:8080/redirected/elsewhere', @client.responses.last.uri.to_s
49
+ assert_equal 200, status
50
+ assert_equal "The redirect worked.\n", body
51
+ end
52
+
53
+ def test_posts_data_using_query_string
54
+ status, headers, body = @client.post('http://localhost:8080/canhaspost', 'mopar=dongs&joux3=king')
55
+ assert_equal 200, status
56
+ assert body =~ /mopar => dongs/
57
+ assert body =~ /joux3 => king/
58
+ end
59
+
60
+ def test_posts_data_using_hash
61
+ status, headers, body = @client.post('http://localhost:8080/canhaspost', {'mopar'=>'dongs', 'joux3'=>'king'})
62
+ assert_equal 200, status
63
+ assert body =~ /mopar => dongs/
64
+ assert body =~ /joux3 => king/
65
+ end
66
+
67
+ def test_head_http_method
68
+ status, headers, body = @client.head('http://localhost:8080/whatmethod')
69
+ assert_equal 'HEAD', headers['method-used']
70
+ end
71
+
72
+ def test_get_http_method
73
+ status, headers, body = @client.get('http://localhost:8080/whatmethod')
74
+ assert_equal 'GET', headers['method-used']
75
+ end
76
+
77
+ def test_post_http_method
78
+ status, headers, body = @client.post('http://localhost:8080/whatmethod', {'some' => 'query'})
79
+ assert_equal 'POST', headers['method-used']
80
+ end
81
+
82
+ def test_put_http_method
83
+ status, headers, body = @client.put('http://localhost:8080/whatmethod', {'some' => 'query'})
84
+ assert_equal 'PUT', headers['method-used']
85
+ end
86
+
87
+ def test_delete_http_method
88
+ status, headers, body = @client.delete('http://localhost:8080/whatmethod')
89
+ assert_equal 'DELETE', headers['method-used']
90
+ end
91
+
92
+ def test_head_requests_return_nil_body
93
+ status, headers, body = @client.head('http://localhost:8080/')
94
+ assert_equal nil, body
95
+ end
96
+
97
+ def test_delete_requests_return_nil_body
98
+ status, headers, body = @client.delete('http://localhost:8080/whatmethod')
99
+ assert_equal nil, body
100
+ end
101
+
102
+ def test_read_timeout
103
+ assert_raise Timeout::Error do
104
+ @client.get('http://localhost:8080/timeout')
105
+ end
106
+ end
107
+
108
+ def test_infinite_redirection
109
+ @client.get('http://localhost:8080/infiniteredirect')
110
+ end
111
+
112
+ def test_basic_auth
113
+ status, headers, body = @client.get('http://localhost:8080/basic_auth')
114
+ assert_equal status, 401 #unauthorized!
115
+
116
+ @client.set_auth('tester', 'wanger')
117
+ status, headers, body = @client.get('http://localhost:8080/basic_auth')
118
+ assert_equal status, 200
119
+ assert body =~ /auth successful/
120
+ end
121
+
122
+ def test_cookie_domain
123
+ cookie = WANG::Cookie.new.parse("x=y; domain=cat.com")
124
+ assert cookie.match_domain?("cat.com")
125
+ assert !cookie.match_domain?("cat.com.au")
126
+ assert !cookie.match_domain?("cat,com") #just incase we are using the regexp '.'
127
+ assert !cookie.match_domain?("dogeatcat.com")
128
+ assert !cookie.match_domain?("ihatethat.cat.com")
129
+
130
+ cookie = WANG::Cookie.new.parse("x=y; domain=.cat.com")
131
+ assert cookie.match_domain?("blah.cat.com")
132
+ assert !cookie.match_domain?("cat.com") #this is what I read in the spec
133
+ assert !cookie.match_domain?("blah.cat.com.au")
134
+ end
135
+
136
+ def test_cookie_paths
137
+ cookie = WANG::Cookie.new.parse("x=y; path=/lamo/")
138
+ assert cookie.match_path?("/lamo")
139
+ assert !cookie.match_path?("/lamoa")
140
+ assert cookie.match_path?("/lamo/aaa/bb")
141
+ assert !cookie.match_path?("/lam")
142
+ assert !cookie.match_path?("/lam/oo")
143
+ end
144
+
145
+ def test_cookie_setting
146
+ status, headers, body = @client.get('http://localhost:8080/cookie_check')
147
+ assert_equal status, 401 #no cookies set
148
+
149
+ status, headers, body = @client.get('http://localhost:8080/cookie_set')
150
+ assert_equal status, 200
151
+
152
+ status, headers, body = @client.get('http://localhost:8080/cookie_check')
153
+ assert_equal status, 200
154
+
155
+ status, headers, body = @client.get('http://localhost:8080/cookie_set2')
156
+ assert_equal status, 200 #set the same cookie to something else
157
+
158
+ status, headers, body = @client.get('http://localhost:8080/cookie_check')
159
+ assert_equal status, 401 #wrong value because we set it again
160
+
161
+ status, headers, body = @client.get('http://localhost:8080/cookie_check2')
162
+ assert_equal status, 200
163
+ end
164
+
165
+ def test_cookie_expiry
166
+ status, headers, body = @client.get('http://localhost:8080/cookie_set_expired')
167
+ assert_equal status, 200
168
+
169
+ status, headers, body = @client.get('http://localhost:8080/cookie_check')
170
+ assert_equal status, 401 #wang should remove the cookie since it expired an hour ago
171
+
172
+ status, headers, body = @client.get('http://localhost:8080/cookie_set_nonexpired')
173
+ assert_equal status, 200
174
+
175
+ status, headers, body = @client.get('http://localhost:8080/cookie_check')
176
+ assert_equal status, 200 #wang should accept the cookie and send it, because it still has an hour till expiry
177
+ end
178
+ end
@@ -0,0 +1,143 @@
1
+ # vim: set noet:
2
+ #
3
+ # WANG - Web Access with No Grief
4
+ # http://github.com/kamu/wang/
5
+
6
+ require 'webrick'
7
+ require 'stringio'
8
+ require 'zlib'
9
+
10
+ class HTTPMethodServlet < WEBrick::HTTPServlet::AbstractServlet
11
+ %w(HEAD GET POST PUT DELETE).each do |http_method|
12
+ define_method("do_#{http_method}") do |request, response|
13
+ response['Method-Used'] = request.request_method
14
+ unless request.request_method == 'DELETE'
15
+ response.body = 'Some meaningless body'
16
+ end
17
+ raise WEBrick::HTTPStatus::OK
18
+ end
19
+ end
20
+ end
21
+
22
+ class WANGTestServer
23
+ class BlackHole
24
+ def <<(x)
25
+ end
26
+ end
27
+
28
+ def initialize
29
+ log = WEBrick::Log.new(BlackHole.new)
30
+ @server = WEBrick::HTTPServer.new(:Port => 8080, :AccessLog => [], :Logger => log)
31
+
32
+ @server.mount_proc('/redirect') do |request, response|
33
+ response['Location'] = '/redirected/elsewhere'
34
+ raise WEBrick::HTTPStatus::MovedPermanently
35
+ end
36
+ @server.mount_proc('/redirected/elsewhere') do |request, response|
37
+ response.body = "The redirect worked.\n"
38
+ response['Content-Type'] = 'text/plain'
39
+ raise WEBrick::HTTPStatus::OK
40
+ end
41
+ @server.mount_proc('/') do |request, response|
42
+ response.body = "<html><head><title>hi</title></head><body><p>Hullo!</p></body></html>"
43
+ response['Content-Type'] = 'text/html'
44
+ raise WEBrick::HTTPStatus::OK
45
+ end
46
+ @server.mount_proc('/canhaspost') do |request, response|
47
+ response.body = request.query.map do |key, val|
48
+ "#{key} => #{val}"
49
+ end.join("\n")
50
+ response['Content-Type'] = 'text/plain'
51
+ raise WEBrick::HTTPStatus::OK
52
+ end
53
+ @server.mount_proc('/timeout') do |request, response|
54
+ sleep 1
55
+ raise WEBrick::HTTPStatus::OK
56
+ end
57
+ @server.mount_proc('/infiniteredirect') do |request, response|
58
+ response['Location'] = '/infiniteredirect'
59
+ raise WEBrick::HTTPStatus::TemporaryRedirect
60
+ end
61
+ @server.mount_proc('/basic_auth') do |request, response|
62
+ WEBrick::HTTPAuth.basic_auth(request, response, "WANG basic HTTP auth test") {|user, pass|
63
+ user == 'tester' && pass == 'wanger'
64
+ }
65
+ response.body = "Basic auth successful!"
66
+ raise WEBrick::HTTPStatus::OK
67
+ end
68
+ @htdigest = WEBrick::HTTPAuth::Htdigest.new('test/htdigest')
69
+ @authenticator = WEBrick::HTTPAuth::DigestAuth.new(
70
+ :UserDB => @htdigest,
71
+ :Realm => 'WANG digest HTTP auth test'
72
+ )
73
+ @server.mount_proc('/digest_auth') do |request, response|
74
+ @authenticator.authenticate(request, response)
75
+ response.body = "Digest auth successful!"
76
+ raise WEBrick::HTTPStatus::OK
77
+ end
78
+ @server.mount('/whatmethod', HTTPMethodServlet)
79
+ @server.mount_proc('/cookie_check') do |request, response|
80
+ request.cookies.each do |cookie|
81
+ raise WEBrick::HTTPStatus::OK if cookie.name.eql?("wangcookie") and cookie.value.eql?("getyerhandoffmywang")
82
+ end
83
+ raise WEBrick::HTTPStatus::Unauthorized
84
+ end
85
+ @server.mount_proc('/cookie_set') do |request, response|
86
+ cookie = WEBrick::Cookie.new('wangcookie', 'getyerhandoffmywang')
87
+ response.cookies << cookie
88
+ raise WEBrick::HTTPStatus::OK
89
+ end
90
+ @server.mount_proc('/cookie_set2') do |request, response|
91
+ cookie = WEBrick::Cookie.new('wangcookie', 'touchmywang')
92
+ response.cookies << cookie
93
+ raise WEBrick::HTTPStatus::OK
94
+ end
95
+ @server.mount_proc('/cookie_check2') do |request, response|
96
+ request.cookies.each do |cookie|
97
+ raise WEBrick::HTTPStatus::OK if cookie.name.eql?("wangcookie") and cookie.value.eql?("touchmywang")
98
+ end
99
+ raise WEBrick::HTTPStatus::Unauthorized
100
+ end
101
+ @server.mount_proc('/cookie_set_expired') do |request, response|
102
+ cookie = WEBrick::Cookie.new('wangcookie', 'getyerhandoffmywang')
103
+ cookie.expires = (Time.now - 60*60*1)
104
+ response.cookies << cookie
105
+ raise WEBrick::HTTPStatus::OK
106
+ end
107
+
108
+ @server.mount_proc('/cookie_set_nonexpired') do |request, response|
109
+ cookie = WEBrick::Cookie.new('wangcookie', 'getyerhandoffmywang')
110
+ cookie.expires = (Time.now + 60*60*1)
111
+ response.cookies << cookie
112
+ raise WEBrick::HTTPStatus::OK
113
+ end
114
+ @server.mount_proc('/test_inflate') do |request, response|
115
+ response['content-encoding'] = 'deflate'
116
+ response.body = Zlib::Deflate.deflate("inflate worked")
117
+ raise WEBrick::HTTPStatus::OK
118
+ end
119
+ @server.mount_proc('/test_gzip') do |request, response|
120
+ response['content-encoding'] = 'gzip'
121
+ output = StringIO.open('', 'w')
122
+ gz = Zlib::GzipWriter.new(output)
123
+ gz.write("gzip worked")
124
+ gz.close
125
+ response.body = output.string
126
+ raise WEBrick::HTTPStatus::OK
127
+ end
128
+ end
129
+
130
+ def start
131
+ @server.start
132
+ end
133
+
134
+ def shutdown
135
+ @server.shutdown
136
+ end
137
+ end
138
+
139
+ if __FILE__ == $0
140
+ server = WANGTestServer.new
141
+ trap('INT') { server.shutdown }
142
+ server.start
143
+ end
data/wang.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = 'wang'
4
+ s.version = "0.02"
5
+ s.summary = "Web Access with No Grief."
6
+ s.authors = ["Kamu", "Joux3"]
7
+ s.email = "mr.kamu@gmail.com"
8
+ s.homepage = "http://github.com/kamu/wang/tree"
9
+ s.requirements << 'none'
10
+ s.require_path = 'lib'
11
+ s.files = ["rakefile", "wang.gemspec", "lib/wang.rb", "test/wang_test.rb", "test/wang_test_server.rb", "test/htdigest"]
12
+ s.test_files = ["test/wang_test.rb"]
13
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kamu-wang
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.02"
5
+ platform: ruby
6
+ authors:
7
+ - Kamu
8
+ - Joux3
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-06-05 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email: mr.kamu@gmail.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - rakefile
27
+ - wang.gemspec
28
+ - lib/wang.rb
29
+ - test/wang_test.rb
30
+ - test/wang_test_server.rb
31
+ - test/htdigest
32
+ has_rdoc: false
33
+ homepage: http://github.com/kamu/wang/tree
34
+ post_install_message:
35
+ rdoc_options: []
36
+
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements:
52
+ - none
53
+ rubyforge_project:
54
+ rubygems_version: 1.0.1
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: Web Access with No Grief.
58
+ test_files:
59
+ - test/wang_test.rb