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 +437 -0
- data/rakefile +34 -0
- data/test/htdigest +1 -0
- data/test/wang_test.rb +178 -0
- data/test/wang_test_server.rb +143 -0
- data/wang.gemspec +13 -0
- metadata +59 -0
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
|