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