http_streaming_client 0.8.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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/AUTHORS +1 -0
- data/CHANGELOG +20 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +138 -0
- data/Rakefile +14 -0
- data/http_streaming_client.gemspec +28 -0
- data/lib/http_streaming_client/client.rb +349 -0
- data/lib/http_streaming_client/credentials/.gitignore +2 -0
- data/lib/http_streaming_client/credentials/adobe.rb.sample +14 -0
- data/lib/http_streaming_client/credentials/twitter.rb.sample +10 -0
- data/lib/http_streaming_client/custom_logger.rb +85 -0
- data/lib/http_streaming_client/decoders/gzip.rb +121 -0
- data/lib/http_streaming_client/errors.rb +48 -0
- data/lib/http_streaming_client/oauth/adobe.rb +78 -0
- data/lib/http_streaming_client/oauth/base.rb +70 -0
- data/lib/http_streaming_client/oauth/twitter.rb +94 -0
- data/lib/http_streaming_client/oauth.rb +34 -0
- data/lib/http_streaming_client/railtie.rb +38 -0
- data/lib/http_streaming_client/version.rb +32 -0
- data/lib/http_streaming_client.rb +36 -0
- data/spec/adobe_spec.rb +137 -0
- data/spec/client_spec.rb +98 -0
- data/spec/oauth_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/twitter_spec.rb +66 -0
- data/tools/adobe_firehose.rb +40 -0
- data/tools/adobe_firehose_performance_test.rb +83 -0
- data/tools/generate_twitter_authorization_header.rb +31 -0
- data/tools/generate_twitter_bearer_token.rb +26 -0
- data/tools/twitter_firehose.rb +17 -0
- data/tools/twitter_firehose_performance_test.rb +78 -0
- metadata +168 -0
@@ -0,0 +1,349 @@
|
|
1
|
+
###########################################################################
|
2
|
+
##
|
3
|
+
## http_streaming_client
|
4
|
+
##
|
5
|
+
## Ruby HTTP client with support for HTTP 1.1 streaming, GZIP compressed
|
6
|
+
## streams, and chunked transfer encoding. Includes extensible OAuth
|
7
|
+
## support for the Adobe Analytics Firehose and Twitter Streaming APIs.
|
8
|
+
##
|
9
|
+
## David Tompkins -- 11/8/2013
|
10
|
+
## tompkins@adobe_dot_com
|
11
|
+
##
|
12
|
+
###########################################################################
|
13
|
+
##
|
14
|
+
## Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
|
15
|
+
##
|
16
|
+
## Licensed under the Apache License, Version 2.0 (the "License");
|
17
|
+
## you may not use this file except in compliance with the License.
|
18
|
+
## You may obtain a copy of the License at
|
19
|
+
##
|
20
|
+
## http://www.apache.org/licenses/LICENSE-2.0
|
21
|
+
##
|
22
|
+
## Unless required by applicable law or agreed to in writing, software
|
23
|
+
## distributed under the License is distributed on an "AS IS" BASIS,
|
24
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
25
|
+
## See the License for the specific language governing permissions and
|
26
|
+
## limitations under the License.
|
27
|
+
##
|
28
|
+
###########################################################################
|
29
|
+
|
30
|
+
require 'socket'
|
31
|
+
require 'uri'
|
32
|
+
require 'zlib'
|
33
|
+
|
34
|
+
require "http_streaming_client/version"
|
35
|
+
require "http_streaming_client/custom_logger"
|
36
|
+
require "http_streaming_client/errors"
|
37
|
+
require "http_streaming_client/decoders/gzip"
|
38
|
+
|
39
|
+
module HttpStreamingClient
|
40
|
+
|
41
|
+
class Client
|
42
|
+
|
43
|
+
attr_accessor :socket, :interrupted, :compression_requested
|
44
|
+
|
45
|
+
ALLOWED_MIME_TYPES = ["application/json", "text/plain", "text/html"]
|
46
|
+
|
47
|
+
def self.logger
|
48
|
+
HttpStreamingClient.logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger
|
52
|
+
HttpStreamingClient.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(opts = {})
|
56
|
+
logger.debug("Client.new: #{opts}")
|
57
|
+
@socket = nil
|
58
|
+
@interrupted = false
|
59
|
+
@compression_requested = opts[:compression].nil? ? true : opts[:compression]
|
60
|
+
logger.debug("compression is #{@compression_requested}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.get(uri, opts = {}, &block)
|
64
|
+
logger.debug("get:#{uri}")
|
65
|
+
self.new.request("GET", uri, opts, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get(uri, opts = {}, &block)
|
69
|
+
logger.debug("get(interrupt):#{uri}")
|
70
|
+
@interrupted = false
|
71
|
+
begin
|
72
|
+
request("GET", uri, opts, &block)
|
73
|
+
rescue IOError => e
|
74
|
+
raise e unless @interrupted
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.post(uri, body, opts = {}, &block)
|
79
|
+
logger.debug("post:#{uri}")
|
80
|
+
self.new.request("POST", uri, opts.merge({:body => body}), &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
def post(uri, body, opts = {}, &block)
|
84
|
+
logger.debug("post(interrupt):#{uri}")
|
85
|
+
@interrupted = false
|
86
|
+
begin
|
87
|
+
request("POST", uri, opts.merge({:body => body}), &block)
|
88
|
+
rescue IOError => e
|
89
|
+
raise e unless @interrupted
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def interrupt
|
94
|
+
logger.debug("interrupt")
|
95
|
+
@interrupted = true
|
96
|
+
@socket.close unless @socket.nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
def request(method, uri, opts = {}, &block)
|
100
|
+
logger.debug("Client::request:#{method}:#{uri}:#{opts}")
|
101
|
+
|
102
|
+
if uri.is_a?(String)
|
103
|
+
uri = URI.parse(uri)
|
104
|
+
end
|
105
|
+
|
106
|
+
default_headers = {
|
107
|
+
"User-Agent" => opts["User-Agent"] || "HttpStreamingClient #{HttpStreamingClient::VERSION}",
|
108
|
+
"Accept" => "*/*",
|
109
|
+
"Accept-Charset" => "utf-8"
|
110
|
+
}
|
111
|
+
|
112
|
+
if method == "POST" || method == "PUT"
|
113
|
+
default_headers["Content-Type"] = opts["Content-Type"] || "application/x-www-form-urlencoded;charset=UTF-8"
|
114
|
+
body = opts.delete(:body)
|
115
|
+
if body.is_a?(Hash)
|
116
|
+
body = body.keys.collect {|param| "#{URI.escape(param.to_s)}=#{URI.escape(body[param].to_s)}"}.join('&')
|
117
|
+
end
|
118
|
+
default_headers["Content-Length"] = body.length
|
119
|
+
end
|
120
|
+
|
121
|
+
unless uri.userinfo.nil?
|
122
|
+
default_headers["Authorization"] = "Basic #{[uri.userinfo].pack('m').strip!}\r\n"
|
123
|
+
end
|
124
|
+
|
125
|
+
encodings = []
|
126
|
+
encodings << "gzip" if (@compression_requested and opts[:compression].nil?) or opts[:compression]
|
127
|
+
if encodings.any?
|
128
|
+
default_headers["Accept-Encoding"] = "#{encodings.join(',')}"
|
129
|
+
end
|
130
|
+
|
131
|
+
headers = default_headers.merge(opts[:headers] || {})
|
132
|
+
logger.debug "request headers: #{headers}"
|
133
|
+
|
134
|
+
socket = initialize_socket(uri, opts)
|
135
|
+
request = "#{method} #{uri.path}#{uri.query ? "?"+uri.query : nil} HTTP/1.1\r\n"
|
136
|
+
request << "Host: #{uri.host}\r\n"
|
137
|
+
headers.each do |k, v|
|
138
|
+
request << "#{k}: #{v}\r\n"
|
139
|
+
end
|
140
|
+
request << "\r\n"
|
141
|
+
if method == "POST"
|
142
|
+
request << body
|
143
|
+
end
|
144
|
+
|
145
|
+
socket.write(request)
|
146
|
+
|
147
|
+
response_head = {}
|
148
|
+
response_head[:headers] = {}
|
149
|
+
|
150
|
+
socket.each_line do |line|
|
151
|
+
if line == "\r\n" then
|
152
|
+
break
|
153
|
+
else
|
154
|
+
header = line.split(": ")
|
155
|
+
if header.size == 1
|
156
|
+
header = header[0].split(" ")
|
157
|
+
response_head[:version] = header[0]
|
158
|
+
response_head[:code] = header[1].to_i
|
159
|
+
response_head[:msg] = header[2]
|
160
|
+
logger.debug "HTTP response code is #{response_head[:code]}"
|
161
|
+
else
|
162
|
+
response_head[:headers][camelize_header_name(header[0])] = header[1].strip
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
logger.debug "response headers:#{response_head[:headers]}"
|
168
|
+
|
169
|
+
content_length = response_head[:headers]["Content-Length"].to_i
|
170
|
+
logger.debug "content-length: #{content_length}"
|
171
|
+
|
172
|
+
content_type = response_head[:headers]["Content-Type"].split(';').first
|
173
|
+
logger.debug "content-type: #{content_type}"
|
174
|
+
|
175
|
+
response_compression = false
|
176
|
+
|
177
|
+
if ALLOWED_MIME_TYPES.include?(content_type)
|
178
|
+
case response_head[:headers]["Content-Encoding"]
|
179
|
+
when "gzip"
|
180
|
+
response_compression = true
|
181
|
+
end
|
182
|
+
else
|
183
|
+
raise InvalidContentType, "invalid response MIME type: #{content_type}"
|
184
|
+
end
|
185
|
+
|
186
|
+
if (response_head[:code] != 200)
|
187
|
+
s = "Received HTTP #{response_head[:code]} response"
|
188
|
+
logger.debug "request: #{request}"
|
189
|
+
response = socket.read(content_length)
|
190
|
+
logger.debug "response: #{response}"
|
191
|
+
raise HttpError.new(response_head[:code], "Received HTTP #{response_head[:code]} response", response_head[:headers])
|
192
|
+
end
|
193
|
+
|
194
|
+
if response_head[:headers]["Transfer-Encoding"] == 'chunked'
|
195
|
+
partial = nil
|
196
|
+
decoder = nil
|
197
|
+
response = ""
|
198
|
+
|
199
|
+
if response_compression then
|
200
|
+
logger.debug "response compression detected"
|
201
|
+
if block_given? then
|
202
|
+
decoder = HttpStreamingClient::Decoders::GZip.new { |line|
|
203
|
+
logger.debug "read #{line.size} uncompressed bytes"
|
204
|
+
block.call(line) unless @interrupted }
|
205
|
+
else
|
206
|
+
decoder = HttpStreamingClient::Decoders::GZip.new { |line|
|
207
|
+
logger.debug "read #{line.size} uncompressed bytes, #{response.size} bytes total"
|
208
|
+
response << line unless @interrupted }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
while !socket.eof? && (line = socket.gets)
|
213
|
+
chunkLeft = 0
|
214
|
+
|
215
|
+
if line.match /^0*?\r\n/ then
|
216
|
+
logger.debug "received zero length chunk, chunked encoding EOF"
|
217
|
+
break
|
218
|
+
end
|
219
|
+
|
220
|
+
next if line == "\r\n"
|
221
|
+
|
222
|
+
size = line.hex
|
223
|
+
logger.debug "chunk size:#{size}"
|
224
|
+
|
225
|
+
partial = socket.read(size)
|
226
|
+
next if partial.nil?
|
227
|
+
|
228
|
+
remaining = size-partial.size
|
229
|
+
logger.debug "read #{partial.size} bytes, #{remaining} bytes remaining"
|
230
|
+
until remaining == 0
|
231
|
+
partial << socket.read(remaining)
|
232
|
+
remaining = size-partial.size
|
233
|
+
logger.debug "read #{partial.size} bytes, #{remaining} bytes remaining"
|
234
|
+
end
|
235
|
+
|
236
|
+
return if @interrupted
|
237
|
+
|
238
|
+
if response_compression then
|
239
|
+
decoder << partial
|
240
|
+
else
|
241
|
+
if block_given? then
|
242
|
+
yield partial
|
243
|
+
else
|
244
|
+
logger.debug "no block specified, returning chunk results and halting streaming response"
|
245
|
+
response << partial
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
return response
|
251
|
+
|
252
|
+
else
|
253
|
+
# Not chunked transfer encoding, but potentially gzip'd, and potentially streaming with content-length = 0
|
254
|
+
|
255
|
+
if content_length > 0 then
|
256
|
+
bits = socket.read(content_length)
|
257
|
+
logger.debug "read #{content_length} bytes"
|
258
|
+
return bits if !response_compression
|
259
|
+
logger.debug "response compression detected"
|
260
|
+
begin
|
261
|
+
decoder = Zlib::GzipReader.new(StringIO.new(bits))
|
262
|
+
return decoder.read
|
263
|
+
rescue Zlib::Error
|
264
|
+
raise DecoderError
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
if response_compression then
|
269
|
+
|
270
|
+
logger.debug "response compression detected"
|
271
|
+
decoder = nil
|
272
|
+
response = ""
|
273
|
+
|
274
|
+
if block_given? then
|
275
|
+
decoder = HttpStreamingClient::Decoders::GZip.new { |line|
|
276
|
+
logger.debug "read #{line.size} uncompressed bytes"
|
277
|
+
block.call(line) unless @interrupted }
|
278
|
+
else
|
279
|
+
decoder = HttpStreamingClient::Decoders::GZip.new { |line|
|
280
|
+
logger.debug "read #{line.size} uncompressed bytes, #{response.size} bytes total"
|
281
|
+
response << line unless @interrupted }
|
282
|
+
end
|
283
|
+
|
284
|
+
while (!socket.eof? and !(line = socket.read_nonblock(2048)).nil?)
|
285
|
+
logger.debug "read compressed line, #{line.size} bytes"
|
286
|
+
decoder << line
|
287
|
+
break response if @interrupted
|
288
|
+
end
|
289
|
+
logger.debug "EOF detected"
|
290
|
+
decoder = nil
|
291
|
+
|
292
|
+
return response
|
293
|
+
|
294
|
+
else
|
295
|
+
|
296
|
+
response = ""
|
297
|
+
|
298
|
+
while (!socket.eof? and !(line = socket.readline).nil?)
|
299
|
+
if block_given? then
|
300
|
+
yield line
|
301
|
+
logger.debug "read #{line.size} bytes"
|
302
|
+
else
|
303
|
+
logger.debug "read #{line.size} bytes, #{response.size} bytes total"
|
304
|
+
response << line
|
305
|
+
end
|
306
|
+
break if @interrupted
|
307
|
+
end
|
308
|
+
|
309
|
+
return response
|
310
|
+
|
311
|
+
end
|
312
|
+
end
|
313
|
+
ensure
|
314
|
+
logger.debug "ensure socket closed"
|
315
|
+
decoder.close if !decoder.nil?
|
316
|
+
socket.close if !socket.nil? and !socket.closed?
|
317
|
+
end
|
318
|
+
|
319
|
+
private
|
320
|
+
|
321
|
+
def camelize_header_name(header_name)
|
322
|
+
(header_name.split('-').map {|s| s.capitalize}).join('-')
|
323
|
+
end
|
324
|
+
|
325
|
+
def initialize_socket(uri, opts = {})
|
326
|
+
return opts[:socket] if opts[:socket]
|
327
|
+
|
328
|
+
if uri.is_a?(String)
|
329
|
+
uri = URI.parse(uri)
|
330
|
+
end
|
331
|
+
|
332
|
+
@socket = TCPSocket.new(uri.host, uri.port)
|
333
|
+
|
334
|
+
if uri.scheme.eql? "https"
|
335
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
336
|
+
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
337
|
+
@socket = OpenSSL::SSL::SSLSocket.new(@socket, ctx).tap do |socket|
|
338
|
+
socket.sync_close = true
|
339
|
+
socket.connect
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
opts.merge!({:socket => @socket})
|
344
|
+
@interrupted = false
|
345
|
+
return opts[:socket]
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
###########################################################################
|
2
|
+
##
|
3
|
+
## http_streaming_client
|
4
|
+
##
|
5
|
+
## Ruby HTTP client with support for HTTP 1.1 streaming, GZIP compressed
|
6
|
+
## streams, and chunked transfer encoding. Includes extensible OAuth
|
7
|
+
## support for the Adobe Analytics Firehose and Twitter Streaming APIs.
|
8
|
+
##
|
9
|
+
## David Tompkins -- 11/8/2013
|
10
|
+
## tompkins@adobe_dot_com
|
11
|
+
##
|
12
|
+
###########################################################################
|
13
|
+
##
|
14
|
+
## Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
|
15
|
+
##
|
16
|
+
## Licensed under the Apache License, Version 2.0 (the "License");
|
17
|
+
## you may not use this file except in compliance with the License.
|
18
|
+
## You may obtain a copy of the License at
|
19
|
+
##
|
20
|
+
## http://www.apache.org/licenses/LICENSE-2.0
|
21
|
+
##
|
22
|
+
## Unless required by applicable law or agreed to in writing, software
|
23
|
+
## distributed under the License is distributed on an "AS IS" BASIS,
|
24
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
25
|
+
## See the License for the specific language governing permissions and
|
26
|
+
## limitations under the License.
|
27
|
+
##
|
28
|
+
###########################################################################
|
29
|
+
|
30
|
+
require 'logger'
|
31
|
+
|
32
|
+
module HttpStreamingClient
|
33
|
+
|
34
|
+
class ColoredLogFormatter < Logger::Formatter
|
35
|
+
|
36
|
+
SEVERITY_TO_COLOR_MAP = {'DEBUG'=>'32', 'INFO'=>'0;37', 'WARN'=>'35', 'ERROR'=>'31', 'FATAL'=>'31', 'UNKNOWN'=>'37'}
|
37
|
+
|
38
|
+
def call(severity, time, progname, msg)
|
39
|
+
color = SEVERITY_TO_COLOR_MAP[severity]
|
40
|
+
"\033[0;37m[%s] \033[#{color}m%5s - %s\033[0m\n" % [time.to_s, severity, msg]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class CustomLoggerInternal
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@console = nil
|
48
|
+
@logfile = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(name, *args)
|
52
|
+
if !@console.nil?
|
53
|
+
@console.method(name).call(args) unless name.to_s =~ /(unknown|fatal|error|warn|info|debug)/
|
54
|
+
@console.method(name).call(args[0])
|
55
|
+
end
|
56
|
+
@logfile.method(name).call(args[0]) unless @logfile.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
def logfile=(enable)
|
60
|
+
return (@logfile = nil) if !enable
|
61
|
+
@logfile = Logger.new("test.log")
|
62
|
+
@logfile.formatter = ColoredLogFormatter.new
|
63
|
+
@logfile.level = Logger::DEBUG
|
64
|
+
end
|
65
|
+
|
66
|
+
def console=(enable)
|
67
|
+
return (@console = nil) if !enable
|
68
|
+
@console = Logger.new(STDOUT)
|
69
|
+
@console.formatter = ColoredLogFormatter.new
|
70
|
+
@console.level = Logger::INFO
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
@custom_logger_internal = nil
|
76
|
+
|
77
|
+
def self.logger
|
78
|
+
return @custom_logger_internal unless @custom_logger_internal.nil?
|
79
|
+
return @custom_logger_internal = CustomLoggerInternal.new
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.logger=(logger)
|
83
|
+
@custom_logger_internal = logger
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
###########################################################################
|
2
|
+
##
|
3
|
+
## http_streaming_client
|
4
|
+
##
|
5
|
+
## Ruby HTTP client with support for HTTP 1.1 streaming, GZIP compressed
|
6
|
+
## streams, and chunked transfer encoding. Includes extensible OAuth
|
7
|
+
## support for the Adobe Analytics Firehose and Twitter Streaming APIs.
|
8
|
+
##
|
9
|
+
## David Tompkins -- 11/8/2013
|
10
|
+
## tompkins@adobe_dot_com
|
11
|
+
##
|
12
|
+
###########################################################################
|
13
|
+
##
|
14
|
+
## Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
|
15
|
+
##
|
16
|
+
## Licensed under the Apache License, Version 2.0 (the "License");
|
17
|
+
## you may not use this file except in compliance with the License.
|
18
|
+
## You may obtain a copy of the License at
|
19
|
+
##
|
20
|
+
## http://www.apache.org/licenses/LICENSE-2.0
|
21
|
+
##
|
22
|
+
## Unless required by applicable law or agreed to in writing, software
|
23
|
+
## distributed under the License is distributed on an "AS IS" BASIS,
|
24
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
25
|
+
## See the License for the specific language governing permissions and
|
26
|
+
## limitations under the License.
|
27
|
+
##
|
28
|
+
###########################################################################
|
29
|
+
|
30
|
+
require 'zlib'
|
31
|
+
|
32
|
+
module HttpStreamingClient
|
33
|
+
|
34
|
+
module Decoders
|
35
|
+
|
36
|
+
class GZip
|
37
|
+
|
38
|
+
def logger
|
39
|
+
HttpStreamingClient.logger
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(&packet_callback)
|
43
|
+
logger.debug "GZip:initialize"
|
44
|
+
@packet_callback = packet_callback
|
45
|
+
end
|
46
|
+
|
47
|
+
def <<(compressed_packet)
|
48
|
+
return unless compressed_packet && compressed_packet.size > 0
|
49
|
+
decompressed_packet = decompress(compressed_packet)
|
50
|
+
process_decompressed_packet(decompressed_packet)
|
51
|
+
end
|
52
|
+
|
53
|
+
def close
|
54
|
+
logger.debug "GZip:close"
|
55
|
+
decompressed_packet = ""
|
56
|
+
begin
|
57
|
+
@gzip ||= Zlib::GzipReader.new @buf
|
58
|
+
decompressed_packet = @gzip.readline
|
59
|
+
rescue Zlib::Error
|
60
|
+
raise DecoderError
|
61
|
+
end
|
62
|
+
process_decompressed_packet(decompressed_packet)
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def decompress(compressed_packet)
|
68
|
+
@buf ||= GZipBufferIO.new
|
69
|
+
@buf << compressed_packet
|
70
|
+
|
71
|
+
# pass at least 2k bytes to GzipReader to avoid zlib EOF
|
72
|
+
if @buf.size > 2048
|
73
|
+
@gzip ||= Zlib::GzipReader.new @buf
|
74
|
+
@gzip.readline
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class GZipBufferIO
|
79
|
+
|
80
|
+
def logger
|
81
|
+
HttpStreamingClient.logger
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(string="")
|
85
|
+
logger.debug "GZipBufferIO:initialize"
|
86
|
+
@packet_stream = string
|
87
|
+
end
|
88
|
+
|
89
|
+
def <<(string)
|
90
|
+
@packet_stream << string
|
91
|
+
end
|
92
|
+
|
93
|
+
# called by GzipReader
|
94
|
+
def read(length=nil, buffer=nil)
|
95
|
+
logger.debug "GZipBufferIO:read:packet_stream:#{@packet_stream.nil? ? 'nil' : 'not nil'}"
|
96
|
+
buffer ||= ""
|
97
|
+
length ||= 0
|
98
|
+
buffer << @packet_stream[0..(length-1)]
|
99
|
+
@packet_stream = @packet_stream[length..-1]
|
100
|
+
buffer
|
101
|
+
end
|
102
|
+
|
103
|
+
# called by GzipReader
|
104
|
+
def size
|
105
|
+
@packet_stream.size
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def process_decompressed_packet(decompressed_packet)
|
112
|
+
logger.debug "GZipBufferIO:process_decompressed_packet:size:#{decompressed_packet.nil? ? "nil" : decompressed_packet.size}"
|
113
|
+
if decompressed_packet && decompressed_packet.size > 0
|
114
|
+
@packet_callback.call(decompressed_packet)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
###########################################################################
|
2
|
+
##
|
3
|
+
## http_streaming_client
|
4
|
+
##
|
5
|
+
## Ruby HTTP client with support for HTTP 1.1 streaming, GZIP compressed
|
6
|
+
## streams, and chunked transfer encoding. Includes extensible OAuth
|
7
|
+
## support for the Adobe Analytics Firehose and Twitter Streaming APIs.
|
8
|
+
##
|
9
|
+
## David Tompkins -- 11/8/2013
|
10
|
+
## tompkins@adobe_dot_com
|
11
|
+
##
|
12
|
+
###########################################################################
|
13
|
+
##
|
14
|
+
## Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
|
15
|
+
##
|
16
|
+
## Licensed under the Apache License, Version 2.0 (the "License");
|
17
|
+
## you may not use this file except in compliance with the License.
|
18
|
+
## You may obtain a copy of the License at
|
19
|
+
##
|
20
|
+
## http://www.apache.org/licenses/LICENSE-2.0
|
21
|
+
##
|
22
|
+
## Unless required by applicable law or agreed to in writing, software
|
23
|
+
## distributed under the License is distributed on an "AS IS" BASIS,
|
24
|
+
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
25
|
+
## See the License for the specific language governing permissions and
|
26
|
+
## limitations under the License.
|
27
|
+
##
|
28
|
+
###########################################################################
|
29
|
+
|
30
|
+
module HttpStreamingClient
|
31
|
+
|
32
|
+
class InvalidContentType < Exception; end
|
33
|
+
|
34
|
+
class HttpTimeOut < StandardError; end
|
35
|
+
|
36
|
+
class HttpError < StandardError
|
37
|
+
|
38
|
+
attr_reader :status, :message, :headers
|
39
|
+
|
40
|
+
def initialize(status, message, headers = nil)
|
41
|
+
super "#{status}:#{message}"
|
42
|
+
@status = status
|
43
|
+
@message = message
|
44
|
+
@headers = headers
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|