kamu-wang 0.02
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.
- 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
|