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.
- data/lib/distelli/clientframework.rb +417 -0
- 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: []
|