raptor-io 0.0.1

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.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. 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