http_streaming_client 0.8.1

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