raptor-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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