DistelliClientFramework 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/distelli/clientframework.rb +417 -0
  2. metadata +61 -0
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'set'
4
+ require 'logger'
5
+ require 'distelli/servicemarshallers'
6
+ require 'securerandom'
7
+ require 'net/http'
8
+ require 'uri'
9
+ require 'date'
10
+ require 'openssl'
11
+ require 'base64'
12
+
13
+ module Distelli
14
+ module Logging
15
+ def logger
16
+ Logging.logger
17
+ end
18
+
19
+ def self.logger
20
+ @logger ||= Logger.new(STDOUT)
21
+ end
22
+ end
23
+
24
+ class ClientException < StandardError
25
+ end
26
+
27
+ class Credentials
28
+ attr_accessor :key_id, :secret_key
29
+ def initialize(key_id, secret_key)
30
+ @key_id = key_id
31
+ @secret_key = secret_key
32
+ end
33
+ end
34
+
35
+ class RequestSigner
36
+ include Distelli::Logging
37
+ def initialize()
38
+ end
39
+
40
+ def get_string_to_sign(request_info)
41
+ string_to_sign = Array.new
42
+ string_to_sign.push(request_info.http_method)
43
+ if request_info.contains_header(ServiceConstants::CONTENT_MD5_HEADER)
44
+ string_to_sign.push(request_info.headers[ServiceConstants::CONTENT_MD5_HEADER])
45
+ else
46
+ string_to_sign.push("")
47
+ end
48
+
49
+ if request_info.contains_header(ServiceConstants::CONTENT_TYPE_HEADER)
50
+ string_to_sign.push(request_info.headers[ServiceConstants::CONTENT_TYPE_HEADER])
51
+ else
52
+ string_to_sign.push("")
53
+ end
54
+
55
+ if request_info.contains_header(ServiceConstants::DATE_HEADER)
56
+ string_to_sign.push(request_info.headers[ServiceConstants::DATE_HEADER])
57
+ else
58
+ string_to_sign.push("")
59
+ end
60
+
61
+ sorted_headers = Array.new
62
+ request_info.headers.each do |header|
63
+ if header[0].to_s().downcase().start_with?("x-dstli")
64
+ sorted_headers.push(header[0])
65
+ end
66
+ end
67
+
68
+ sorted_headers.sort!()
69
+ sorted_headers.each do |header|
70
+ string_to_sign.push(header.downcase()+":"+request_info.headers[header.to_s])
71
+ end
72
+
73
+ # Canonicalize the resource
74
+ if request_info.resource_uri == nil
75
+ string_to_sign.push("")
76
+ else
77
+ string_to_sign.push(request_info.resource_uri)
78
+ end
79
+
80
+ sorted_params = Array.new
81
+ if request_info.query_params != nil
82
+ request_info.query_params.each_pair do |param, values|
83
+ if values == nil or values.length() == 0
84
+ next
85
+ end
86
+ values.sort!()
87
+ query_param_list = Array.new
88
+ values.each do |value|
89
+ query_param_list.push(value.to_s)
90
+ end
91
+ sorted_params.push(param+"="+query_param_list.join(','))
92
+ end
93
+ end
94
+
95
+ sorted_params.sort!()
96
+ string_to_sign.push(sorted_params.join('&'))
97
+ return string_to_sign.join("\n")
98
+ end
99
+
100
+ def get_signature(secret_key, request_info)
101
+ string_to_sign = get_string_to_sign(request_info)
102
+ logger.debug("StringToSign:\n"+string_to_sign)
103
+ return hmacsha(secret_key, string_to_sign)
104
+ end
105
+
106
+ def hmacsha(secret_key, string_to_sign)
107
+ digest = OpenSSL::Digest::Digest.new("sha256")
108
+ hmac = OpenSSL::HMAC.digest(digest, secret_key, string_to_sign)
109
+ return Base64.encode64(hmac)
110
+ end
111
+ end
112
+
113
+ class ClientBase
114
+ include Distelli::Logging
115
+ def initialize(credentials, endpoint=nil)
116
+ @credentials = credentials
117
+ @endpoint = endpoint
118
+ @json_marshaller = JsonMarshaller.new
119
+ @xml_marshaller = XmlMarshaller.new
120
+ @logger = nil
121
+ end
122
+
123
+ def set_endpoint(endpoint)
124
+ if endpoint == nil
125
+ return
126
+ end
127
+ if endpoint.start_with?("http://") or endpoint.start_with?("https://")
128
+ @endpoint = endpoint
129
+ else
130
+ @endpoint = "http://"+endpoint
131
+ end
132
+
133
+ end
134
+
135
+ def execute(request_info)
136
+ if @endpoint == nil
137
+ raise ClientException.new("Invalid endpoint: "+@endpoint.to_s)
138
+ end
139
+ logger.debug("Executing request to endpoint: "+@endpoint.to_s)
140
+ # uri_str = @endpoint
141
+ # if request_info.resource_uri != nil
142
+ # uri_str = uri_str+request_info.resource_uri
143
+ # end
144
+
145
+ request_info.add_header(ServiceConstants::OPERATION_HEADER, request_info.operation)
146
+ request_info.add_header(ServiceConstants::CONTENT_TYPE_HEADER, request_info.content_type)
147
+
148
+ if not request_info.contains_header(ServiceConstants::RESPONSE_TYPE_HEADER)
149
+ request_info.add_header(ServiceConstants::RESPONSE_TYPE_HEADER, request_info.response_type)
150
+ end
151
+
152
+ request_info.remove_query_params(ServiceConstants::OPERATION_PARAM)
153
+ request_info.remove_query_params(ServiceConstants::RESPONSE_TYPE_PARAM)
154
+
155
+ if request_info.request_id == nil
156
+ request_info.request_id = SecureRandom.uuid
157
+ end
158
+
159
+ request_info.add_header(ServiceConstants::REQUEST_ID_HEADER, request_info.request_id)
160
+ request_info.remove_query_params(ServiceConstants::REQUEST_ID_PARAM)
161
+
162
+ # Set the Date header
163
+ cur_time = Time.now.utc
164
+ request_info.add_header(ServiceConstants::DATE_HEADER, cur_time.strftime("%a, %d %b %Y %H:%M:%S %z"))
165
+
166
+ # Create the URI
167
+ uri = URI.parse(request_info.get_uri(@endpoint))
168
+
169
+ # TODO: Set the Content-MD5 header
170
+
171
+ # Set the authentication headers
172
+ if @credentials != nil
173
+ signer = RequestSigner.new
174
+ signature = signer.get_signature(@credentials.secret_key, request_info)
175
+ request_info.add_header(ServiceConstants::AUTHORIZATION_HEADER, "DWS "+@credentials.key_id+":"+signature)
176
+ end
177
+
178
+ response = nil
179
+
180
+ http_method = request_info.http_method
181
+ http_client = Net::HTTP.new(uri.host, uri.port)
182
+ http_client.set_debug_output($stdout)
183
+ if http_method == ServiceConstants::HTTP_METHOD_GET
184
+ logger.debug("Executing GET: "+request_info.to_s)
185
+ # Create the http request with the URI, Query Params and Headers
186
+ http_request = Net::HTTP::Get.new(uri.request_uri, request_info.headers)
187
+ # Execute the request to get the response
188
+ response = http_client.request(http_request)
189
+ elsif http_method == ServiceConstants::HTTP_METHOD_POST
190
+ logger.debug("Executing POST: "+request_info.to_s)
191
+ payload = get_payload(request_info)
192
+ # Create the http request with the URI, Query Params and Headers
193
+ http_request = Net::HTTP::Post.new(uri.request_uri, request_info.headers)
194
+ # Set the body
195
+ http_request.body = payload
196
+ # Execute the request to get the response
197
+ response = http_client.request(http_request)
198
+ elsif http_method == ServiceConstants::HTTP_METHOD_PUT
199
+ logger.debug("Executing PUT: "+request_info.to_s)
200
+ payload = get_payload(request_info)
201
+ # Create the http request with the URI, Query Params and Headers
202
+ http_request = Net::HTTP::Put.new(uri.request_uri, request_info.headers)
203
+ # Set the body
204
+ http_request.body= payload
205
+ # Execute the request to get the response
206
+ response = http_client.request(http_request)
207
+ elsif http_method == ServiceConstants::HTTP_METHOD_DELETE
208
+ logger.debug("Executing DELETE: "+request_info.to_s)
209
+ # Create the http request with the URI, Query Params and Headers
210
+ http_request = Net::HTTP::Delete.new(uri.request_uri, request_info.headers)
211
+ # Execute the request to get the response
212
+ response = http_client.request(http_request)
213
+ else
214
+ raise ClientException.new("Invalid HTTP Method: "+http_method)
215
+ end
216
+ return handle_response(response)
217
+ end
218
+
219
+ private
220
+ def get_payload(request_info)
221
+ if request_info == nil
222
+ return nil
223
+ end
224
+ request = request_info.request
225
+ if request != nil
226
+ if request_info.content_type == nil or request_info.content_type == ServiceConstants::CONTENT_TYPE_JSON
227
+ marshalled_payload = @json_marshaller.marshall(request)
228
+ logger.debug("Marshalled Payload: "+marshalled_payload)
229
+ return marshalled_payload
230
+ elsif request_info.content_type == ServiceConstants.CONTENT_TYPE_XML
231
+ marshalled_payload = @xml_marshaller.marshall(request)
232
+ logger.debug("Marshalled Payload: "+marshalled_payload)
233
+ return marshalled_payload
234
+ end
235
+ end
236
+ end
237
+
238
+ def handle_response(response)
239
+ http_status_code = response.code
240
+ content_type = response[ServiceConstants::CONTENT_TYPE_HEADER]
241
+ logger.debug("Received Response "+http_status_code.to_s+", Content-Type: "+content_type.to_s+", Payload: "+response.body.to_s)
242
+ if http_status_code.to_i/100 != 2
243
+ error = nil
244
+ if content_type != nil
245
+ error = unmarshall_error(content_type, response)
246
+ end
247
+
248
+ if http_status_code.to_i/100 == 4
249
+ raise Distelli::ClientError.new("Request Failed! Client Error.",error[0], error[1], http_status_code.to_i)
250
+ elsif http_status_code.to_i/100 == 5
251
+ raise Distelli::ServerError.new("Request Failed! Server Error", error[0], error[1], http_status_code.to_i)
252
+ else
253
+ raise ClientException.new("Invalid Response Status Code: "+http_status_code)
254
+ end
255
+ end
256
+
257
+ if content_type != nil
258
+ return unmarshall_response(content_type, response)
259
+ end
260
+ end
261
+
262
+ def unmarshall_response(content_type, response)
263
+ if content_type == nil
264
+ raise ClientException.new("Cannot unmarshall with missing content type: "+content_type)
265
+ end
266
+ if content_type == ServiceConstants::CONTENT_TYPE_JSON
267
+ return @json_marshaller.unmarshall(response.body)
268
+ elsif content_type == ServiceConstants::CONTENT_TYPE_XML
269
+ return @xml_marshaller.unmarshall(response.body)
270
+ elsif content_type == ServiceConstants::CONTENT_TYPE_OCTET_STREAM
271
+ raise ClientException.new("Content-Type: "+content_type+" not supported by this client!")
272
+ else
273
+ raise ClientException.new("Invalid Content type: "+content_type)
274
+ end
275
+ end
276
+
277
+ def unmarshall_error(content_type, response)
278
+ if content_type == nil
279
+ raise ClientException.new("Invalid content type: "+content_type+" Failed to unmarshall error response: "+response.body)
280
+ end
281
+ if content_type == ServiceConstants::CONTENT_TYPE_JSON
282
+ return @json_marshaller.unmarshall_error(response.body)
283
+ elsif content_type == distelli.constants.ServiceConstants.CONTENT_TYPE_XML
284
+ return @xml_marshaller.unmarshall_error(response.body)
285
+ else
286
+ raise ClientException.new("Unsupported Content Type: "+content_type+" while unmarshallinge error_response: "+response.body)
287
+ end
288
+ end
289
+ end
290
+
291
+ class RequestInfo
292
+ attr_accessor :query_params, :headers, :resource_uri, :operation, :request_id, :request, :request_bytes
293
+ VALID_CONTENT_TYPES = Set.new [ServiceConstants::CONTENT_TYPE_JSON, ServiceConstants::CONTENT_TYPE_XML, ServiceConstants::CONTENT_TYPE_OCTET_STREAM]
294
+ VALID_HTTP_METHODS = Set.new [ServiceConstants::HTTP_METHOD_GET, ServiceConstants::HTTP_METHOD_PUT, ServiceConstants::HTTP_METHOD_POST, ServiceConstants::HTTP_METHOD_DELETE]
295
+ VALID_RESPONSE_TYPES = Set.new [ServiceConstants::RESPONSE_TYPE_XML, ServiceConstants::RESPONSE_TYPE_JSON]
296
+ def initialize()
297
+ @query_params = Hash.new
298
+ @headers = Hash.new
299
+ @resource_uri = "/"
300
+ @operation = nil
301
+ @request_id = nil
302
+ @content_type = ServiceConstants::CONTENT_TYPE_JSON
303
+ @http_method = "GET"
304
+ @request = nil
305
+ @request_bytes = nil
306
+ @response_type = ServiceConstants::RESPONSE_TYPE_JSON
307
+ end
308
+
309
+ # Returns tho fully qualified uri by concatenating the endpoint
310
+ # and the resource_uri and adding the query params
311
+ def get_uri(endpoint)
312
+ uriparse_result = URI.parse(endpoint)
313
+ uri_str = endpoint
314
+ resource_uri = uriparse_result.path
315
+ if resource_uri == nil or resource_uri.length() == 0
316
+ resource_uri = "/"
317
+ uri_str = endpoint+resource_uri
318
+ end
319
+
320
+ @resource_uri = resource_uri
321
+ # now build the query string
322
+ query_param_list = Array.new
323
+ @query_params.each_pair do |k,v|
324
+ v.each do |x|
325
+ query_param_list.push(k+'='+x)
326
+ end
327
+ end
328
+ return uri_str+'?'+query_param_list.join('&')
329
+ end
330
+
331
+ #Lets implement the to_s method of the RequestInfo
332
+ def to_s
333
+ vars = instance_variables.map do |n|
334
+ "#{n}=#{instance_variable_get(n).inspect}"
335
+ end
336
+ "{%s}" % [vars.join(', ')]
337
+ end
338
+
339
+ def content_type
340
+ @content_type
341
+ end
342
+
343
+ def content_type=(content_type)
344
+ if !VALID_CONTENT_TYPES.include?(content_type)
345
+ raise ClientException.new("Invalid content type: "+content_type)
346
+ end
347
+ @content_type = content_type
348
+ end
349
+
350
+ def response_type
351
+ @response_type
352
+ end
353
+
354
+ def response_type=(response_type)
355
+ if !VALID_RESPONSE_TYPES.include?(response_type)
356
+ raise ClientException.new("Invalid response type: "+response_type)
357
+ end
358
+ @response_type = response_type
359
+ end
360
+
361
+ def http_method
362
+ @http_method
363
+ end
364
+
365
+ def http_method=(http_method)
366
+ if !VALID_HTTP_METHODS.include?(http_method)
367
+ raise ClientException.new("Invalid http method: "+http_method)
368
+ end
369
+ @http_method = http_method
370
+ end
371
+
372
+ def contains_header(header)
373
+ return @headers.has_key?(header)
374
+ end
375
+
376
+ def add_header(header, value)
377
+ if header == nil or value == nil
378
+ return
379
+ end
380
+ @headers[header] = value
381
+ end
382
+
383
+ def remove_header(header)
384
+ if header == nil
385
+ return
386
+ end
387
+ @headers.delete(header)
388
+ end
389
+
390
+ def remove_query_params(key)
391
+ if not @query_params.has_key?(key)
392
+ return
393
+ end
394
+ @query_params.delete(key)
395
+ end
396
+
397
+ def add_query_param(key, value)
398
+ if value == nil
399
+ return
400
+ end
401
+ param_values = nil
402
+ if @query_params.has_key?(key)
403
+ param_values = @query_params[key]
404
+ else
405
+ param_values = Array.new
406
+ @query_params[key] = param_values
407
+ end
408
+
409
+ if value.instance_of?(DateTime)
410
+ #TODO: Check if the timezone is present. if not then set it to UTC
411
+ param_values.push(value.strftime("%Y-%m-%dT%H:%M:%S%z"))
412
+ else
413
+ param_values.push(value.to_s)
414
+ end
415
+ end
416
+ end
417
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: DistelliClientFramework
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rahul Singh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: DistelliServiceMarshallers
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Distelli Client Framework for Ruby Clients
31
+ email: rsingh@distelli.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/distelli/clientframework.rb
37
+ homepage: http://www.distelli.com/
38
+ licenses: []
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 1.8.23
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: Distelli Client classes and marshallers for Ruby Clients
61
+ test_files: []