goshrine_bot 0.1.1 → 0.1.5

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.
@@ -1,288 +0,0 @@
1
- #--
2
- #
3
- # Author:: Francis Cianfrocca (gmail: blackhedd)
4
- # Homepage:: http://rubyeventmachine.com
5
- # Date:: 16 July 2006
6
- #
7
- # See EventMachine and EventMachine::Connection for documentation and
8
- # usage examples.
9
- #
10
- #----------------------------------------------------------------------------
11
- #
12
- # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13
- # Gmail: blackhedd
14
- #
15
- # This program is free software; you can redistribute it and/or modify
16
- # it under the terms of either: 1) the GNU General Public License
17
- # as published by the Free Software Foundation; either version 2 of the
18
- # License, or (at your option) any later version; or 2) Ruby's License.
19
- #
20
- # See the file COPYING for complete licensing information.
21
- #
22
- #---------------------------------------------------------------------------
23
- #
24
- #
25
-
26
-
27
-
28
- module GoshrineBot
29
-
30
- # === Usage
31
- #
32
- # EventMachine.run {
33
- # http = EventMachine::Protocols::HttpClient.request(
34
- # :host => server,
35
- # :port => 80,
36
- # :request => "/index.html",
37
- # :query_string => "parm1=value1&parm2=value2"
38
- # )
39
- # http.callback {|response|
40
- # puts response[:status]
41
- # puts response[:headers]
42
- # puts response[:content]
43
- # }
44
- # }
45
- #--
46
- # TODO:
47
- # Add streaming so we can support enormous POSTs. Current max is 20meg.
48
- # Timeout for connections that run too long or hang somewhere in the middle.
49
- # Persistent connections (HTTP/1.1), may need a associated delegate object.
50
- # DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
51
- # DNS lookups are unbelievably slow.
52
- # HEAD requests.
53
- # Chunked transfer encoding.
54
- # Convenience methods for requests. get, post, url, etc.
55
- # SSL.
56
- # Handle status codes like 304, 100, etc.
57
- # Refactor this code so that protocol errors all get handled one way (an exception?),
58
- # instead of sprinkling set_deferred_status :failed calls everywhere.
59
- class HttpClient < EventMachine::Connection
60
- include EventMachine::Deferrable
61
-
62
- MaxPostContentLength = 20 * 1024 * 1024
63
-
64
- # === Arg list
65
- # :host => 'ip/dns', :port => fixnum, :verb => 'GET', :request => 'path',
66
- # :basic_auth => {:username => '', :password => ''}, :content => 'content',
67
- # :contenttype => 'text/plain', :query_string => '', :host_header => '',
68
- # :cookie => ''
69
- def self.request( args = {} )
70
- args[:port] ||= 80
71
- EventMachine.connect( args[:host], args[:port], self ) {|c|
72
- # According to the docs, we will get here AFTER post_init is called.
73
- c.instance_eval {@args = args}
74
- }
75
- end
76
-
77
- def post_init
78
- @start_time = Time.now
79
- @data = ""
80
- @read_state = :base
81
- end
82
-
83
- # We send the request when we get a connection.
84
- # AND, we set an instance variable to indicate we passed through here.
85
- # That allows #unbind to know whether there was a successful connection.
86
- # NB: This naive technique won't work when we have to support multiple
87
- # requests on a single connection.
88
- def connection_completed
89
- @connected = true
90
- send_request @args
91
- end
92
-
93
- def send_request args
94
- args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
95
- args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
96
-
97
- verb = args[:verb].to_s.upcase
98
- unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
99
- set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
100
- return # NOTE THE EARLY RETURN, we're not sending any data.
101
- end
102
-
103
- request = args[:request] || "/"
104
- unless request[0,1] == "/"
105
- request = "/" + request
106
- end
107
-
108
- qs = args[:query_string] || ""
109
- if qs.length > 0 and qs[0,1] != '?'
110
- qs = "?" + qs
111
- end
112
-
113
- version = args[:version] || "1.1"
114
-
115
- # Allow an override for the host header if it's not the connect-string.
116
- host = args[:host_header] || args[:host] || "_"
117
- # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
118
- port = args[:port]
119
-
120
- # POST items.
121
- postcontenttype = args[:contenttype] || "application/octet-stream"
122
- postcontent = args[:content] || ""
123
- raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
124
-
125
- # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
126
- # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
127
- req = [
128
- "#{verb} #{request}#{qs} HTTP/#{version}",
129
- "Host: #{host}:#{port}",
130
- "User-agent: Ruby EventMachine",
131
- ]
132
-
133
- if verb == "POST" || verb == "PUT"
134
- req << "Content-type: #{postcontenttype}"
135
- req << "Content-length: #{postcontent.length}"
136
- end
137
-
138
- # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
139
- # Eventually we will want to deal intelligently with arrays and hashes.
140
- if args[:cookie]
141
- req << "Cookie: #{args[:cookie]}"
142
- end
143
-
144
- # Allow custom HTTP headers, e.g. SOAPAction
145
- args[:custom_headers].each do |k,v|
146
- req << "#{k}: #{v}"
147
- end
148
-
149
- # Basic-auth stanza contributed by Matt Murphy.
150
- if args[:basic_auth]
151
- basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
152
- req << "Authorization: Basic #{basic_auth_string}"
153
- end
154
-
155
- req << ""
156
- reqstring = req.map {|l| "#{l}\r\n"}.join
157
- send_data reqstring
158
-
159
- if verb == "POST" || verb == "PUT"
160
- send_data postcontent
161
- end
162
- end
163
-
164
-
165
- def receive_data data
166
- while data and data.length > 0
167
- case @read_state
168
- when :base
169
- # Perform any per-request initialization here and don't consume any data.
170
- @data = ""
171
- @headers = []
172
- @content_length = nil # not zero
173
- @content = ""
174
- @status = nil
175
- @read_state = :header
176
- @connection_close = nil
177
- when :header
178
- ary = data.split( /\r?\n/m, 2 )
179
- if ary.length == 2
180
- data = ary.last
181
- if ary.first == ""
182
- if (@content_length and @content_length > 0) || @chunked || @connection_close
183
- @read_state = :content
184
- else
185
- dispatch_response
186
- @read_state = :base
187
- end
188
- else
189
- @headers << ary.first
190
- if @headers.length == 1
191
- parse_response_line
192
- elsif ary.first =~ /\Acontent-length:\s*/i
193
- # Only take the FIRST content-length header that appears,
194
- # which we can distinguish because @content_length is nil.
195
- # TODO, it's actually a fatal error if there is more than one
196
- # content-length header, because the caller is presumptively
197
- # a bad guy. (There is an exploit that depends on multiple
198
- # content-length headers.)
199
- @content_length ||= $'.to_i
200
- elsif ary.first =~ /\Aconnection:\s*close/i
201
- @connection_close = true
202
- elsif ary.first =~ /\Atransfer-encoding:\s*chunked/i
203
- @chunked = true
204
- end
205
- end
206
- else
207
- @data << data
208
- data = ""
209
- end
210
- when :content
211
- if @chunked && @chunk_length
212
- bytes_needed = @chunk_length - @chunk_read
213
- new_data = data[0, bytes_needed]
214
- @chunk_read += new_data.length
215
- @content += new_data
216
- data = data[bytes_needed..-1] || ""
217
- if @chunk_length == @chunk_read && data[0,2] == "\r\n"
218
- @chunk_length = nil
219
- data = data[2..-1]
220
- end
221
- elsif @chunked
222
- if (m = data.match(/\A(\S*)\r\n/m))
223
- data = data[m[0].length..-1]
224
- @chunk_length = m[1].to_i(16)
225
- @chunk_read = 0
226
- if @chunk_length == 0
227
- dispatch_response
228
- @read_state = :base
229
- end
230
- end
231
- elsif @content_length
232
- # If there was no content-length header, we have to wait until the connection
233
- # closes. Everything we get until that point is content.
234
- # TODO: Must impose a content-size limit, and also must implement chunking.
235
- # Also, must support either temporary files for large content, or calling
236
- # a content-consumer block supplied by the user.
237
- bytes_needed = @content_length - @content.length
238
- @content += data[0, bytes_needed]
239
- data = data[bytes_needed..-1] || ""
240
- if @content_length == @content.length
241
- dispatch_response
242
- @read_state = :base
243
- end
244
- else
245
- @content << data
246
- data = ""
247
- end
248
- end
249
- end
250
- end
251
-
252
-
253
- # We get called here when we have received an HTTP response line.
254
- # It's an opportunity to throw an exception or trigger other exceptional
255
- # handling.
256
- def parse_response_line
257
- if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
258
- @status = $1.to_i
259
- else
260
- set_deferred_status :failed, {
261
- :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
262
- }
263
- close_connection
264
- end
265
- end
266
- private :parse_response_line
267
-
268
- def dispatch_response
269
- @read_state = :base
270
- set_deferred_status :succeeded, {
271
- :content => @content,
272
- :headers => @headers,
273
- :status => @status
274
- }
275
- # TODO, we close the connection for now, but this is wrong for persistent clients.
276
- close_connection
277
- end
278
-
279
- def unbind
280
- if !@connected
281
- set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
282
- elsif (@read_state == :content and @content_length == nil)
283
- dispatch_response
284
- end
285
- end
286
- end
287
-
288
- end