raptor-io 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +30 -0
- data/README.md +51 -0
- data/lib/rack/handler/raptor-io.rb +130 -0
- data/lib/raptor-io.rb +11 -0
- data/lib/raptor-io/error.rb +19 -0
- data/lib/raptor-io/protocol.rb +6 -0
- data/lib/raptor-io/protocol/error.rb +10 -0
- data/lib/raptor-io/protocol/http.rb +34 -0
- data/lib/raptor-io/protocol/http/client.rb +685 -0
- data/lib/raptor-io/protocol/http/error.rb +16 -0
- data/lib/raptor-io/protocol/http/headers.rb +132 -0
- data/lib/raptor-io/protocol/http/message.rb +67 -0
- data/lib/raptor-io/protocol/http/request.rb +307 -0
- data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
- data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
- data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
- data/lib/raptor-io/protocol/http/response.rb +166 -0
- data/lib/raptor-io/protocol/http/server.rb +446 -0
- data/lib/raptor-io/ruby.rb +4 -0
- data/lib/raptor-io/ruby/hash.rb +24 -0
- data/lib/raptor-io/ruby/ipaddr.rb +15 -0
- data/lib/raptor-io/ruby/openssl.rb +23 -0
- data/lib/raptor-io/ruby/string.rb +27 -0
- data/lib/raptor-io/socket.rb +175 -0
- data/lib/raptor-io/socket/comm.rb +143 -0
- data/lib/raptor-io/socket/comm/local.rb +94 -0
- data/lib/raptor-io/socket/comm/sapni.rb +75 -0
- data/lib/raptor-io/socket/comm/socks.rb +237 -0
- data/lib/raptor-io/socket/comm_chain.rb +30 -0
- data/lib/raptor-io/socket/error.rb +45 -0
- data/lib/raptor-io/socket/switch_board.rb +183 -0
- data/lib/raptor-io/socket/switch_board/route.rb +42 -0
- data/lib/raptor-io/socket/tcp.rb +231 -0
- data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
- data/lib/raptor-io/socket/tcp_server.rb +16 -0
- data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
- data/lib/raptor-io/socket/udp.rb +0 -0
- data/lib/raptor-io/version.rb +6 -0
- data/lib/tasks/yard.rake +26 -0
- data/spec/rack/handler/raptor_spec.rb +140 -0
- data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
- data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
- data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
- data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
- data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
- data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
- data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
- data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
- data/spec/raptor-io/ruby/hash_spec.rb +20 -0
- data/spec/raptor-io/ruby/string_spec.rb +20 -0
- data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
- data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
- data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
- data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
- data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
- data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
- data/spec/raptor-io/socket/tcp_spec.rb +14 -0
- data/spec/raptor-io/socket_spec.rb +16 -0
- data/spec/raptor-io/version_spec.rb +10 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
- data/spec/support/lib/path_helpers.rb +11 -0
- data/spec/support/lib/webserver_option_parser.rb +26 -0
- data/spec/support/lib/webservers.rb +120 -0
- data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
- data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
- data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
- data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
- data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
- data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
- data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
- data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
- metadata +336 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module RaptorIO
|
4
|
+
module Protocol::HTTP
|
5
|
+
class Request
|
6
|
+
|
7
|
+
module Manipulators
|
8
|
+
module Authenticators
|
9
|
+
|
10
|
+
#
|
11
|
+
# Implements HTTP Digest authentication as per RFC2069.
|
12
|
+
#
|
13
|
+
# @see http://tools.ietf.org/html/rfc2069
|
14
|
+
# @see http://en.wikipedia.org/wiki/Digest_access_authentication
|
15
|
+
#
|
16
|
+
# @author Tasos Laskos
|
17
|
+
#
|
18
|
+
class Digest < Manipulator
|
19
|
+
|
20
|
+
def run
|
21
|
+
request.headers['Authorization'] = {
|
22
|
+
'Digest username' => username,
|
23
|
+
realm: challenge[:realm],
|
24
|
+
nonce: challenge[:nonce],
|
25
|
+
uri: request.resource,
|
26
|
+
qop: challenge[:qop],
|
27
|
+
nc: nc,
|
28
|
+
cnonce: cnonce,
|
29
|
+
response: response,
|
30
|
+
algorithm: algorithm_name,
|
31
|
+
opaque: challenge[:opaque]
|
32
|
+
}.map { |k, v| "#{k}=\"#{v}\"" }.join( ', ' )
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def algorithm_klass
|
38
|
+
if challenge[:algorithm].to_s =~ /(.+)(-sess)?$/
|
39
|
+
case $1
|
40
|
+
when 'MD5' then ::Digest::MD5
|
41
|
+
when 'SHA1' then ::Digest::SHA1
|
42
|
+
when 'SHA2' then ::Digest::SHA2
|
43
|
+
when 'SHA256' then ::Digest::SHA256
|
44
|
+
when 'SHA384' then ::Digest::SHA384
|
45
|
+
when 'SHA512' then ::Digest::SHA512
|
46
|
+
when 'RMD160' then ::Digest::RMD160
|
47
|
+
else raise Error, "Unknown algorithm \"#{$1}\"."
|
48
|
+
end
|
49
|
+
else
|
50
|
+
::Digest::MD5
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def algorithm_name
|
55
|
+
algorithm_klass.to_s.split( '::' ).last
|
56
|
+
end
|
57
|
+
|
58
|
+
def sess?
|
59
|
+
challenge[:algorithm].to_s.include? '-sess'
|
60
|
+
end
|
61
|
+
|
62
|
+
def H( data )
|
63
|
+
algorithm_klass.hexdigest( data )
|
64
|
+
end
|
65
|
+
|
66
|
+
def A1
|
67
|
+
without_sess = [ username, challenge[:realm], password ] * ':'
|
68
|
+
|
69
|
+
if sess?
|
70
|
+
H( [without_sess, challenge[:nonce], cnonce ] * ':' )
|
71
|
+
else
|
72
|
+
without_sess
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def A2
|
77
|
+
[ request.http_method.to_s.upcase, request.resource ] * ':'
|
78
|
+
end
|
79
|
+
|
80
|
+
def H1
|
81
|
+
H( A1() )
|
82
|
+
end
|
83
|
+
|
84
|
+
def H2
|
85
|
+
H( A2() )
|
86
|
+
end
|
87
|
+
|
88
|
+
def response
|
89
|
+
if ['auth', 'auth-int'].include? challenge[:qop]
|
90
|
+
return H( [H1(), challenge[:nonce], nc, cnonce, challenge[:qop], H2()] * ':' )
|
91
|
+
end
|
92
|
+
|
93
|
+
H( [H1(), challenge[:nonce], H2()] * ':' )
|
94
|
+
end
|
95
|
+
|
96
|
+
def cnonce
|
97
|
+
[Time.now.to_i.to_s].pack( 'm*' ).strip
|
98
|
+
end
|
99
|
+
|
100
|
+
def nc
|
101
|
+
@nc ||= self.class.nc
|
102
|
+
end
|
103
|
+
def self.nc
|
104
|
+
@nc ||= 0
|
105
|
+
@nc += 1
|
106
|
+
end
|
107
|
+
|
108
|
+
def challenge
|
109
|
+
return @challenge if @challenge
|
110
|
+
|
111
|
+
challenge_options = {}
|
112
|
+
options[:response].headers['www-authenticate'].split( ',' ).each do |pair|
|
113
|
+
matches = pair.strip.match( /(.+)="(.*)"/ )
|
114
|
+
challenge_options[matches[1].to_sym] = matches[2]
|
115
|
+
end
|
116
|
+
challenge_options[:realm] = challenge_options.delete( :'Digest realm' )
|
117
|
+
|
118
|
+
@challenge = challenge_options
|
119
|
+
end
|
120
|
+
|
121
|
+
def username
|
122
|
+
options[:username]
|
123
|
+
end
|
124
|
+
|
125
|
+
def password
|
126
|
+
options[:password]
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'net/ntlm'
|
2
|
+
|
3
|
+
module RaptorIO
|
4
|
+
module Protocol::HTTP
|
5
|
+
class Request
|
6
|
+
|
7
|
+
module Manipulators
|
8
|
+
module Authenticators
|
9
|
+
|
10
|
+
#
|
11
|
+
# Implements HTTP Negotiate authentication.
|
12
|
+
#
|
13
|
+
# @author Tasos Laskos
|
14
|
+
#
|
15
|
+
class Negotiate < Manipulator
|
16
|
+
|
17
|
+
def run
|
18
|
+
return if skip?
|
19
|
+
client.manipulators.delete shortname
|
20
|
+
|
21
|
+
t2 = authorize( type1 ).headers['www-authenticate'].split( ' ' ).last
|
22
|
+
|
23
|
+
if authorize( type3( t2 ) ).code == 401 && client.manipulators['authenticator']
|
24
|
+
client.datastore['authenticator'][:failed] = true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def provider
|
31
|
+
'Negotiate'
|
32
|
+
end
|
33
|
+
|
34
|
+
def authorize( message )
|
35
|
+
client.get( request.url,
|
36
|
+
mode: :sync,
|
37
|
+
manipulators: {
|
38
|
+
'authenticator' => { skip: true },
|
39
|
+
shortname => { skip: true },
|
40
|
+
},
|
41
|
+
headers: { 'Authorization' => "#{provider} #{message}" } )
|
42
|
+
end
|
43
|
+
|
44
|
+
def skip?
|
45
|
+
!!options[:skip]
|
46
|
+
end
|
47
|
+
|
48
|
+
def type1
|
49
|
+
Net::NTLM::Message::Type1.new.encode64
|
50
|
+
end
|
51
|
+
|
52
|
+
def type3( type2 )
|
53
|
+
Net::NTLM::Message.decode64( type2 ).response(
|
54
|
+
{
|
55
|
+
user: options[:username],
|
56
|
+
password: options[:password],
|
57
|
+
domain: options[:domain]
|
58
|
+
},
|
59
|
+
{ ntlmv2: true }
|
60
|
+
).encode64
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RaptorIO
|
2
|
+
module Protocol::HTTP
|
3
|
+
class Request
|
4
|
+
|
5
|
+
module Manipulators
|
6
|
+
module Authenticators
|
7
|
+
|
8
|
+
if !const_defined?( :Negotiate )
|
9
|
+
load File.dirname( __FILE__ ) + '/negotiate.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Implements HTTP NTLM authentication.
|
14
|
+
#
|
15
|
+
# @author Tasos Laskos
|
16
|
+
#
|
17
|
+
class NTLM < Negotiate
|
18
|
+
|
19
|
+
def provider
|
20
|
+
'NTLM'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RaptorIO
|
2
|
+
module Protocol::HTTP
|
3
|
+
class Request
|
4
|
+
|
5
|
+
module Manipulators
|
6
|
+
|
7
|
+
#
|
8
|
+
# Implements automatic HTTP redirect following.
|
9
|
+
#
|
10
|
+
# @author Tasos Laskos
|
11
|
+
#
|
12
|
+
class RedirectFollower < Manipulator
|
13
|
+
|
14
|
+
def run
|
15
|
+
# This request has already been handled.
|
16
|
+
return if request.root_redirect_id
|
17
|
+
|
18
|
+
callbacks = request.callbacks.dup
|
19
|
+
request.clear_callbacks
|
20
|
+
|
21
|
+
request.on_complete do |response|
|
22
|
+
root_redirect_id = request.root_redirect_id ?
|
23
|
+
request.root_redirect_id : request.object_id
|
24
|
+
|
25
|
+
if response.redirect?
|
26
|
+
if redirections[root_redirect_id].size < max
|
27
|
+
redirections[root_redirect_id] << response
|
28
|
+
|
29
|
+
crequest = request.dup
|
30
|
+
crequest.root_redirect_id = root_redirect_id
|
31
|
+
|
32
|
+
# RFC says the Location URI must be a full absolute URL however not
|
33
|
+
# all webapps respect that.
|
34
|
+
crequest.url = crequest.parsed_url.merge( response.headers['Location'] ).to_s
|
35
|
+
|
36
|
+
client.queue( crequest )
|
37
|
+
next
|
38
|
+
else
|
39
|
+
response.redirections = redirections.delete( root_redirect_id )
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
request.callbacks = callbacks
|
44
|
+
request.handle_response response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @return [Hash<Integer, Array<RaptorIO::Protocol::HTTP::Response>]
|
51
|
+
# Keeps track of stacked redirections based on the ID of their root request.
|
52
|
+
def redirections
|
53
|
+
datastore[:redirections] ||= Hash.new { |h, k| h[k] = [] }
|
54
|
+
end
|
55
|
+
|
56
|
+
def max
|
57
|
+
@max ||= (options[:max] || 5).to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module RaptorIO
|
5
|
+
module Protocol::HTTP
|
6
|
+
|
7
|
+
#
|
8
|
+
# HTTP Response.
|
9
|
+
#
|
10
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
11
|
+
#
|
12
|
+
class Response < Message
|
13
|
+
|
14
|
+
# @return [Integer] HTTP response status code.
|
15
|
+
attr_accessor :code
|
16
|
+
|
17
|
+
# @return [String] HTTP response status message.
|
18
|
+
attr_accessor :message
|
19
|
+
|
20
|
+
# @return [Request] HTTP {Request} which triggered this {Response}.
|
21
|
+
attr_accessor :request
|
22
|
+
|
23
|
+
# @return [Array<Response>]
|
24
|
+
# Automatically followed redirections that eventually led to this response.
|
25
|
+
attr_accessor :redirections
|
26
|
+
|
27
|
+
# @return [Exception] Exception representing the error that occurred.
|
28
|
+
attr_accessor :error
|
29
|
+
|
30
|
+
#
|
31
|
+
# @note This class' options are in addition to {Message#initialize}.
|
32
|
+
#
|
33
|
+
# @param [Hash] options Request options.
|
34
|
+
# @option options [Integer] :code HTTP response status code.
|
35
|
+
# @option options [Request] :request HTTP request that triggered this response.
|
36
|
+
#
|
37
|
+
# @see Message#initialize
|
38
|
+
#
|
39
|
+
def initialize( options = {} )
|
40
|
+
super( options )
|
41
|
+
|
42
|
+
@body = @body.force_utf8 if text?
|
43
|
+
@code ||= 0
|
44
|
+
|
45
|
+
# Holds the redirection responses that eventually led to this one.
|
46
|
+
@redirections ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Boolean]
|
50
|
+
# `true` if the response is a `3xx` redirect **and** there is a `Location`
|
51
|
+
# header field.
|
52
|
+
def redirect?
|
53
|
+
code >= 300 && code <= 399 && !!headers['Location']
|
54
|
+
end
|
55
|
+
|
56
|
+
# @note Depends on the response code.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
# `true` if the remote resource has been modified since the date given in
|
60
|
+
# the `If-Modified-Since` request header field, `false` otherwise.
|
61
|
+
def modified?
|
62
|
+
code != 304
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Bool]
|
66
|
+
# `true` if the response body is textual in nature, `false` otherwise
|
67
|
+
# (if binary).
|
68
|
+
def text?
|
69
|
+
return if !@body
|
70
|
+
|
71
|
+
if (type = headers['content-type'])
|
72
|
+
return true if type.start_with?( 'text/' )
|
73
|
+
|
74
|
+
# Non "application/" content types will surely not be text-based
|
75
|
+
# so bail out early.
|
76
|
+
return false if !type.start_with?( 'application/' )
|
77
|
+
end
|
78
|
+
|
79
|
+
# Last resort, more resource intensive binary detection.
|
80
|
+
!@body.binary?
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [String]
|
84
|
+
# String representation of the response.
|
85
|
+
def to_s
|
86
|
+
headers['Content-Length'] = body.to_s.size
|
87
|
+
|
88
|
+
r = "HTTP/#{version} #{code}"
|
89
|
+
r << " #{message}" if message
|
90
|
+
r << "\r\n"
|
91
|
+
r << "#{headers.to_s}\r\n\r\n"
|
92
|
+
r << body.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [String] response HTTP response.
|
96
|
+
# @return [Response]
|
97
|
+
def self.parse( response )
|
98
|
+
options ||= {}
|
99
|
+
|
100
|
+
# FIXME: The existence of this extra newline at the beginning of a
|
101
|
+
# response suggests a bug somewhere else in the response parsing
|
102
|
+
# code.
|
103
|
+
response = response.gsub(/\A\r\n/, '')
|
104
|
+
|
105
|
+
headers_string, options[:body] = response.split( HEADER_SEPARATOR_PATTERN, 2 )
|
106
|
+
request_line = headers_string.to_s.lines.first.to_s.chomp
|
107
|
+
|
108
|
+
options[:version], options[:code], options[:message] =
|
109
|
+
request_line.scan( /HTTP\/([\d.]+)\s+(\d+)\s*(.*)\s*$/ ).flatten
|
110
|
+
|
111
|
+
options.delete(:message) if options[:message].to_s.empty?
|
112
|
+
|
113
|
+
options[:code] = options[:code].to_i
|
114
|
+
|
115
|
+
if !headers_string.to_s.empty?
|
116
|
+
options[:headers] =
|
117
|
+
Headers.parse( headers_string.split( CRLF_PATTERN )[1..-1].join( "\r\n" ) )
|
118
|
+
else
|
119
|
+
options[:headers] = Headers.new
|
120
|
+
end
|
121
|
+
|
122
|
+
if !options[:body].to_s.empty?
|
123
|
+
|
124
|
+
# If any encoding has been applied to the body, remove all evidence of it
|
125
|
+
# and adjust the content-length accordingly.
|
126
|
+
|
127
|
+
case options[:headers]['content-encoding'].to_s.downcase
|
128
|
+
when 'gzip', 'x-gzip'
|
129
|
+
options[:body] = unzip( options[:body] )
|
130
|
+
when 'deflate', 'compress', 'x-compress'
|
131
|
+
options[:body] = inflate( options[:body] )
|
132
|
+
end
|
133
|
+
|
134
|
+
if options[:headers].delete( 'content-encoding' ) ||
|
135
|
+
options[:headers].delete( 'transfer-encoding' )
|
136
|
+
options[:headers]['content-length'] = options[:body].size
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
new( options )
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param [String] str Inflates `str`.
|
144
|
+
# @return [String] Inflated `str`.
|
145
|
+
def self.inflate( str )
|
146
|
+
z = Zlib::Inflate.new
|
147
|
+
s = z.inflate( str )
|
148
|
+
z.close
|
149
|
+
s
|
150
|
+
end
|
151
|
+
|
152
|
+
# @param [String] str Unzips `str`.
|
153
|
+
# @return [String] Unziped `str`.
|
154
|
+
def self.unzip( str )
|
155
|
+
s = ''
|
156
|
+
s.force_encoding( 'ASCII-8BIT' ) if s.respond_to?( :encoding )
|
157
|
+
gz = Zlib::GzipReader.new( StringIO.new( str, 'rb' ) )
|
158
|
+
s << gz.read
|
159
|
+
gz.close
|
160
|
+
s
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|