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.
@@ -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,2 @@
1
+ twitter.rb
2
+ adobe.rb
@@ -0,0 +1,14 @@
1
+ module HttpStreamingClient
2
+ module Credentials
3
+ module Adobe
4
+ VERBOSE=true
5
+ USERNAME=""
6
+ PASSWORD=""
7
+ CLIENTID=""
8
+ CLIENTSECRET=""
9
+ TOKENAPIHOST="https://api.omniture.com/token"
10
+ COMPRESSSTREAM=true
11
+ STREAMURL=""
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module HttpStreamingClient
2
+ module Credentials
3
+ module Twitter
4
+ OAUTH_CONSUMER_KEY = ""
5
+ OAUTH_CONSUMER_SECRET = ""
6
+ OAUTH_TOKEN = ""
7
+ OAUTH_TOKEN_SECRET = ""
8
+ end
9
+ end
10
+ 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