DistelliClientFramework 1.0

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.
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: []