sqs 0.1.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.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/lib/sqs/connection.rb +114 -0
- data/lib/sqs/exceptions.rb +97 -0
- data/lib/sqs/message.rb +36 -0
- data/lib/sqs/parser.rb +48 -0
- data/lib/sqs/queue.rb +95 -0
- data/lib/sqs/roxy/moxie.rb +58 -0
- data/lib/sqs/roxy/proxy.rb +72 -0
- data/lib/sqs/service.rb +118 -0
- data/lib/sqs/signature.rb +60 -0
- data/lib/sqs.rb +26 -0
- data/sqs.gemspec +56 -0
- data/test/signature_test.rb +33 -0
- data/test/test_helper.rb +11 -0
- metadata +73 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jakub Kuźma
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "sqs"
|
10
|
+
gem.summary = %Q{Simple Queue Service accessing library}
|
11
|
+
gem.email = "qoobaa@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/qoobaa/sqs"
|
13
|
+
gem.authors = ["Jakub Kuźma"]
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/*_test.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/*_test.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :default => :test
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
if File.exist?('VERSION.yml')
|
46
|
+
config = YAML.load(File.read('VERSION.yml'))
|
47
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
48
|
+
else
|
49
|
+
version = ""
|
50
|
+
end
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "sqs #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
57
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Sqs
|
2
|
+
|
3
|
+
# Class responsible for handling connections to amazon hosts
|
4
|
+
class Connection
|
5
|
+
include Parser
|
6
|
+
|
7
|
+
attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug
|
8
|
+
alias :use_ssl? :use_ssl
|
9
|
+
|
10
|
+
# ==== Parameters:
|
11
|
+
# +options+:: Hash of options
|
12
|
+
#
|
13
|
+
# ==== Options:
|
14
|
+
# +access_key_id+:: access key id
|
15
|
+
# +secret_access_key+:: secret access key
|
16
|
+
# +use_ssl+:: optional, defaults to false
|
17
|
+
# +debug+:: optional, defaults to false
|
18
|
+
# +timeout+:: optional, for Net::HTTP
|
19
|
+
def initialize(options = {})
|
20
|
+
@access_key_id = options[:access_key_id]
|
21
|
+
@secret_access_key = options[:secret_access_key]
|
22
|
+
@use_ssl = options[:use_ssl] || false
|
23
|
+
@debug = options[:debug]
|
24
|
+
@timeout = options[:timeout]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Makes request with given HTTP method, sets missing parameters,
|
28
|
+
# adds signature to request header and returns response object
|
29
|
+
# (Net::HTTPResponse)
|
30
|
+
#
|
31
|
+
# ==== Parameters:
|
32
|
+
# +options+:: hash of options
|
33
|
+
#
|
34
|
+
# ==== Options:
|
35
|
+
# +host+:: hostname to connect to, optional, defaults to queue.amazonaws.com
|
36
|
+
# +path+:: path to send request to, required, throws ArgumentError if not given
|
37
|
+
#
|
38
|
+
# ==== Returns:
|
39
|
+
# Net::HTTPResponse object -- response from remote server
|
40
|
+
def request(options)
|
41
|
+
host = options.delete(:host) || HOST
|
42
|
+
path = options.delete(:path) or raise ArgumentError, "No path given"
|
43
|
+
|
44
|
+
request = Net::HTTP::Post.new(path)
|
45
|
+
|
46
|
+
response = http(host).start do |http|
|
47
|
+
add_common_options!(options)
|
48
|
+
add_timestamp!(options)
|
49
|
+
add_signature!(host, path, options)
|
50
|
+
set_form_data!(request, options)
|
51
|
+
http.request(request)
|
52
|
+
end
|
53
|
+
|
54
|
+
handle_response(response)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def set_form_data!(request, options)
|
60
|
+
request.set_form_data(options)
|
61
|
+
request.content_type = "application/x-www-form-urlencoded; charset=utf-8"
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_common_options!(options)
|
65
|
+
options.merge!("AWSAccessKeyId" => access_key_id,
|
66
|
+
"SignatureMethod" => "HmacSHA256",
|
67
|
+
"SignatureVersion" => "2",
|
68
|
+
"Version" => "2009-02-01")
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_timestamp!(options)
|
72
|
+
options["Timestamp"] = Time.now.utc.iso8601 if options["Timestamp"].nil? and options["Expires"].nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_signature!(host, path, options)
|
76
|
+
options["Signature"] = Signature.generate(:method => :post,
|
77
|
+
:host => host,
|
78
|
+
:path => path,
|
79
|
+
:access_key_id => access_key_id,
|
80
|
+
:secret_access_key => secret_access_key,
|
81
|
+
:params => options)
|
82
|
+
end
|
83
|
+
|
84
|
+
def port
|
85
|
+
use_ssl ? 443 : 80
|
86
|
+
end
|
87
|
+
|
88
|
+
def http(host)
|
89
|
+
http = Net::HTTP.new(host, port)
|
90
|
+
http.set_debug_output(STDOUT) if @debug
|
91
|
+
http.use_ssl = @use_ssl
|
92
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
|
93
|
+
http.read_timeout = @timeout if @timeout
|
94
|
+
http
|
95
|
+
end
|
96
|
+
|
97
|
+
def handle_response(response)
|
98
|
+
case response.code.to_i
|
99
|
+
when 200...300
|
100
|
+
response
|
101
|
+
when 300...600
|
102
|
+
if response.body.nil? || response.body.empty?
|
103
|
+
raise Error::ResponseError.new(nil, response)
|
104
|
+
else
|
105
|
+
code, message = parse_error(response.body)
|
106
|
+
raise Error::ResponseError.exception(code).new(message, response)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
raise ConnectionError.new(response, "Unknown response code: #{response.code}")
|
110
|
+
end
|
111
|
+
response
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Sqs
|
2
|
+
module Error
|
3
|
+
|
4
|
+
# All responses with a code between 300 and 599 that contain an
|
5
|
+
# <Error></Error> body are wrapped in an ErrorResponse which
|
6
|
+
# contains an Error object. This Error class generates a custom
|
7
|
+
# exception with the name of the xml Error and its message. All
|
8
|
+
# such runtime generated exception classes descend from
|
9
|
+
# ResponseError and contain the ErrorResponse object so that all
|
10
|
+
# code that makes a request can rescue ResponseError and get
|
11
|
+
# access to the ErrorResponse.
|
12
|
+
class ResponseError < StandardError
|
13
|
+
attr_reader :response
|
14
|
+
|
15
|
+
# ==== Parameters:
|
16
|
+
# +message+:: what went wrong
|
17
|
+
# +response+:: Net::HTTPResponse object or nil
|
18
|
+
def initialize(message, response)
|
19
|
+
@response = response
|
20
|
+
super(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Factory for all other Exception classes in module, each for every
|
24
|
+
# error response available from AmazonAWS
|
25
|
+
#
|
26
|
+
# ==== Parameters:
|
27
|
+
# +code+:: code name of exception
|
28
|
+
#
|
29
|
+
# ==== Returns:
|
30
|
+
# Descendant of ResponseError suitable for that exception code or ResponseError class
|
31
|
+
# if no class found
|
32
|
+
def self.exception(code)
|
33
|
+
Sqs::Error.const_get(code)
|
34
|
+
rescue NameError
|
35
|
+
ResponseError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#:stopdoc:
|
40
|
+
|
41
|
+
class AccessDenied < ResponseError; end
|
42
|
+
class AuthFailure < ResponseError; end
|
43
|
+
class ConflictingQueryParameter < ResponseError; end
|
44
|
+
class ElementNotSigned < ResponseError; end
|
45
|
+
class InternalError < ResponseError; end
|
46
|
+
class InvalidAccessKeyId < ResponseError; end
|
47
|
+
class InvalidAction < ResponseError; end
|
48
|
+
class InvalidAddress < ResponseError; end
|
49
|
+
class InvalidAttributeName < ResponseError; end
|
50
|
+
class InvalidHttpRequest < ResponseError; end
|
51
|
+
class InvalidMessageContents < ResponseError; end
|
52
|
+
class InvalidParameterCombination < ResponseError; end
|
53
|
+
class InvalidParameterValue < ResponseError; end
|
54
|
+
class InvalidQueryParameter < ResponseError; end
|
55
|
+
class InvalidRequest < ResponseError; end
|
56
|
+
class InvalidSecurity < ResponseError; end
|
57
|
+
class InvalidSecurityToken < ResponseError; end
|
58
|
+
class MalformedSOAPSignature < ResponseError; end
|
59
|
+
class MalformedVersion < ResponseError; end
|
60
|
+
class MessageTooLong < ResponseError; end
|
61
|
+
class MissingClientTokenId < ResponseError; end
|
62
|
+
class MissingCredentials < ResponseError; end
|
63
|
+
class MissingParameter < ResponseError; end
|
64
|
+
class NoSuchVersion < ResponseError; end
|
65
|
+
class NonExistentQueue < ResponseError; end
|
66
|
+
class NotAuthorizedToUseVersion < ResponseError; end
|
67
|
+
class QueueDeletedRecently < ResponseError; end
|
68
|
+
class QueueNameExists < ResponseError; end
|
69
|
+
class ReadCountOutOfRange < ResponseError; end
|
70
|
+
class RequestExpired < ResponseError; end
|
71
|
+
class RequestThrottled < ResponseError; end
|
72
|
+
class SOAP11IncorrectDateFormat < ResponseError; end
|
73
|
+
class SOAP11MissingAction < ResponseError; end
|
74
|
+
class ServiceUnavailable < ResponseError; end
|
75
|
+
class SignatureDoesNotMatch < ResponseError; end
|
76
|
+
class SoapBodyMissing < ResponseError; end
|
77
|
+
class SoapEnvelopeMissing < ResponseError; end
|
78
|
+
class SoapEnvelopeParseError < ResponseError; end
|
79
|
+
class UnknownEnvelopeNamespace < ResponseError; end
|
80
|
+
class WSSecurityCorruptSignedInfo < ResponseError; end
|
81
|
+
class WSSecurityCreatedDateIncorrectFormat < ResponseError; end
|
82
|
+
class WSSecurityEncodingTypeError < ResponseError; end
|
83
|
+
class WSSecurityExpiresDateIncorrectFormat < ResponseError; end
|
84
|
+
class WSSecurityIncorrectValuetype < ResponseError; end
|
85
|
+
class WSSecurityMissingValuetype < ResponseError; end
|
86
|
+
class WSSecurityMultipleCredentialError < ResponseError; end
|
87
|
+
class WSSecurityMultipleX509Error < ResponseError; end
|
88
|
+
class WSSecuritySignatureError < ResponseError; end
|
89
|
+
class WSSecuritySignatureMissing < ResponseError; end
|
90
|
+
class WSSecuritySignedInfoMissing < ResponseError; end
|
91
|
+
class WSSecurityTimestampExpired < ResponseError; end
|
92
|
+
class WSSecurityTimestampExpiresMissing < ResponseError; end
|
93
|
+
class WSSecurityTimestampMissing < ResponseError; end
|
94
|
+
class WSSecurityX509CertCredentialError < ResponseError; end
|
95
|
+
class X509ParseError < ResponseError; end
|
96
|
+
end
|
97
|
+
end
|
data/lib/sqs/message.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sqs
|
2
|
+
class Message
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_instance_delegators :queue, :name, :queue_request
|
6
|
+
attr_reader :queue, :id, :body, :body_md5, :receipt_handle
|
7
|
+
private_class_method :new
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
self.id == other.id and self.queue == other.queue
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy
|
14
|
+
delete_message
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect #:nodoc:
|
19
|
+
"#<#{self.class}:#{name}/#{id}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def initialize(queue, options)
|
25
|
+
@queue = queue
|
26
|
+
@id = options[:id]
|
27
|
+
@body = options[:body]
|
28
|
+
@body_md5 = options[:body_md5]
|
29
|
+
@receipt_handle = options[:receipt_handle]
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_message
|
33
|
+
queue_request("Action" => "DeleteMessage", "ReceiptHandle" => receipt_handle)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/sqs/parser.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Sqs
|
2
|
+
module Parser
|
3
|
+
include REXML
|
4
|
+
|
5
|
+
def rexml_document(xml)
|
6
|
+
xml.force_encoding(Encoding::UTF_8) if xml.respond_to? :force_encoding
|
7
|
+
Document.new(xml)
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_list_queues_result(xml)
|
11
|
+
urls = []
|
12
|
+
rexml_document(xml).elements.each("ListQueuesResponse/ListQueuesResult/QueueUrl") { |e| urls << e.text }
|
13
|
+
urls
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_create_queue_result(xml)
|
17
|
+
rexml_document(xml).elements["CreateQueueResponse/CreateQueueResult/QueueUrl"].text
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_get_queue_attributes_result(xml)
|
21
|
+
attributes = {}
|
22
|
+
rexml_document(xml).elements.each("GetQueueAttributesResponse/GetQueueAttributesResult/Attribute") do |e|
|
23
|
+
name = e.elements["Name"].text
|
24
|
+
value = e.elements["Value"].text
|
25
|
+
attributes[name] = value
|
26
|
+
end
|
27
|
+
attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_receive_message_result(xml)
|
31
|
+
messages = []
|
32
|
+
rexml_document(xml).elements.each("ReceiveMessageResponse/ReceiveMessageResult/Message") do |e|
|
33
|
+
messages << {
|
34
|
+
:id => e.elements["MessageId"].text,
|
35
|
+
:receipt_handle => e.elements["ReceiptHandle"].text,
|
36
|
+
:body_md5 => e.elements["MD5OfBody"].text,
|
37
|
+
:body => e.elements["Body"].text
|
38
|
+
}
|
39
|
+
end
|
40
|
+
messages
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_error(xml)
|
44
|
+
document = rexml_document(xml)
|
45
|
+
[document.elements["ErrorResponse/Error/Code"].text.split(".").last, document.elements["ErrorResponse/Error/Message"].text]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/sqs/queue.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module Sqs
|
2
|
+
class Queue
|
3
|
+
extend Forwardable
|
4
|
+
include Parser
|
5
|
+
|
6
|
+
attr_reader :path, :name, :service
|
7
|
+
def_instance_delegators :service, :service_request
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
self.name == other.name and self.service == other.service
|
12
|
+
end
|
13
|
+
|
14
|
+
def destroy
|
15
|
+
delete_queue
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def attributes
|
20
|
+
get_queue_attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_attributes(attributes)
|
24
|
+
set_queue_attributes(attributes)
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_message(body)
|
29
|
+
send_message("MessageBody" => body)
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def message(visibility_timeout = nil)
|
34
|
+
options = {}
|
35
|
+
options["VisibilityTimeout"] = visibility_timeout.to_s if visibility_timeout
|
36
|
+
receive_message(options).first
|
37
|
+
end
|
38
|
+
|
39
|
+
def inspect #:nodoc:
|
40
|
+
"#<#{self.class}:#{name}>"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def initialize(service, url) #:nodoc:
|
46
|
+
self.service = service
|
47
|
+
self.url = url
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_writer :service, :path
|
51
|
+
|
52
|
+
def url=(url)
|
53
|
+
parsed_url = URI.parse(url)
|
54
|
+
self.path = parsed_url.path[1..-1]
|
55
|
+
self.name = parsed_url.path.split("/").last
|
56
|
+
end
|
57
|
+
|
58
|
+
def name=(name)
|
59
|
+
@name = name
|
60
|
+
end
|
61
|
+
|
62
|
+
def queue_request(options = {})
|
63
|
+
service_request(options.merge(:path => path))
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_queue
|
67
|
+
queue_request("Action" => "DeleteQueue")
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_queue_attributes
|
71
|
+
response = queue_request("Action" => "GetQueueAttributes", "AttributeName" => "All")
|
72
|
+
parse_get_queue_attributes_result(response.body)
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_queue_attributes(options)
|
76
|
+
attributes = {}
|
77
|
+
options.each_with_index do |attribute, i|
|
78
|
+
attributes["Attribute.#{i + 1}.Name"] = attribute.first.to_s
|
79
|
+
attributes["Attribute.#{i + 1}.Value"] = attribute.last.to_s
|
80
|
+
end
|
81
|
+
queue_request(attributes.merge("Action" => "SetQueueAttributes"))
|
82
|
+
end
|
83
|
+
|
84
|
+
def send_message(options)
|
85
|
+
queue_request(options.merge("Action" => "SendMessage"))
|
86
|
+
end
|
87
|
+
|
88
|
+
def receive_message(options)
|
89
|
+
response = queue_request(options.merge("Action" => "ReceiveMessage"))
|
90
|
+
parse_receive_message_result(response.body).map do |message_attributes|
|
91
|
+
Message.send(:new, self, message_attributes)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright (c) 2008 Ryan Daigle
|
2
|
+
|
3
|
+
# Permission is hereby granted, free of charge, to any person
|
4
|
+
# obtaining a copy of this software and associated documentation files
|
5
|
+
# (the "Software"), to deal in the Software without restriction,
|
6
|
+
# including without limitation the rights to use, copy, modify, merge,
|
7
|
+
# publish, distribute, sublicense, and/or sell copies of the Software,
|
8
|
+
# and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
18
|
+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
19
|
+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
module Sqs
|
24
|
+
module Roxy # :nodoc:all
|
25
|
+
module Moxie
|
26
|
+
# Set up this class to proxy on the given name
|
27
|
+
def proxy(name, options = {}, &block)
|
28
|
+
|
29
|
+
# Make sure args are OK
|
30
|
+
original_method = method_defined?(name) ? instance_method(name) : nil
|
31
|
+
raise "Cannot proxy an existing method, \"#{name}\", and also have a :to option. Please use one or the other." if
|
32
|
+
original_method and options[:to]
|
33
|
+
|
34
|
+
# If we're proxying an existing method, we need to store
|
35
|
+
# the original method and move it out of the way so
|
36
|
+
# we can take over
|
37
|
+
if original_method
|
38
|
+
new_method = "proxied_#{name}"
|
39
|
+
alias_method new_method, "#{name}"
|
40
|
+
options[:to] = original_method
|
41
|
+
end
|
42
|
+
|
43
|
+
# Thanks to Jerry for this simplification of my original class_eval approach
|
44
|
+
# http://ryandaigle.com/articles/2008/11/10/implement-ruby-proxy-objects-with-roxy/comments/8059#comment-8059
|
45
|
+
if !original_method or original_method.arity == 0
|
46
|
+
define_method name do
|
47
|
+
@proxy_for ||= {}
|
48
|
+
@proxy_for[name] ||= Proxy.new(self, options, nil, &block)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
define_method name do |*args|
|
52
|
+
Proxy.new(self, options, args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Copyright (c) 2008 Ryan Daigle
|
2
|
+
|
3
|
+
# Permission is hereby granted, free of charge, to any person
|
4
|
+
# obtaining a copy of this software and associated documentation files
|
5
|
+
# (the "Software"), to deal in the Software without restriction,
|
6
|
+
# including without limitation the rights to use, copy, modify, merge,
|
7
|
+
# publish, distribute, sublicense, and/or sell copies of the Software,
|
8
|
+
# and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
18
|
+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
19
|
+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
module Sqs
|
24
|
+
module Roxy # :nodoc:all
|
25
|
+
# The very simple proxy class that provides a basic pass-through
|
26
|
+
# mechanism between the proxy owner and the proxy target.
|
27
|
+
class Proxy
|
28
|
+
|
29
|
+
alias :proxy_instance_eval :instance_eval
|
30
|
+
alias :proxy_extend :extend
|
31
|
+
|
32
|
+
# Make sure the proxy is as dumb as it can be.
|
33
|
+
# Blatanly taken from Jim Wierich's BlankSlate post:
|
34
|
+
# http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
|
35
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^proxy_|^object_id)/ }
|
36
|
+
|
37
|
+
def initialize(owner, options, args, &block)
|
38
|
+
@owner = owner
|
39
|
+
@target = options[:to]
|
40
|
+
@args = args
|
41
|
+
|
42
|
+
# Adorn with user-provided proxy methods
|
43
|
+
[options[:extend]].flatten.each { |ext| proxy_extend(ext) } if options[:extend]
|
44
|
+
proxy_instance_eval &block if block_given?
|
45
|
+
end
|
46
|
+
|
47
|
+
def proxy_owner
|
48
|
+
@owner
|
49
|
+
end
|
50
|
+
|
51
|
+
def proxy_target
|
52
|
+
if @target.is_a?(Proc)
|
53
|
+
@target.call(@owner)
|
54
|
+
elsif @target.is_a?(UnboundMethod)
|
55
|
+
bound_method = @target.bind(proxy_owner)
|
56
|
+
bound_method.arity == 0 ? bound_method.call : bound_method.call(*@args)
|
57
|
+
else
|
58
|
+
@target
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# def inspect
|
63
|
+
# "#<S3::Roxy::Proxy:0x#{object_id.to_s(16)}>"
|
64
|
+
# end
|
65
|
+
|
66
|
+
# Delegate all method calls we don't know about to target object
|
67
|
+
def method_missing(sym, *args, &block)
|
68
|
+
proxy_target.__send__(sym, *args, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/sqs/service.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module Sqs
|
2
|
+
class Service
|
3
|
+
extend Roxy::Moxie
|
4
|
+
include Parser
|
5
|
+
|
6
|
+
attr_reader :access_key_id, :secret_access_key, :use_ssl
|
7
|
+
|
8
|
+
# Compares service to other, by access_key_id and secret_access_key
|
9
|
+
def ==(other)
|
10
|
+
self.access_key_id == other.access_key_id and self.secret_access_key == other.secret_access_key
|
11
|
+
end
|
12
|
+
|
13
|
+
# ==== Parameters:
|
14
|
+
# +options+:: a hash of options described below
|
15
|
+
#
|
16
|
+
# ==== Options:
|
17
|
+
# +access_key_id+:: Amazon access key id, required
|
18
|
+
# +secret_access_key+:: Amazon secret access key, required
|
19
|
+
# +use_ssl+:: true if use ssl in connection, otherwise false
|
20
|
+
# +timeout+:: parameter for Net::HTTP module
|
21
|
+
# +debug+:: prints the raw requests to STDOUT
|
22
|
+
def initialize(options)
|
23
|
+
@access_key_id = options[:access_key_id] or raise ArgumentError, "No access key id given"
|
24
|
+
@secret_access_key = options[:secret_access_key] or raise ArgumentError, "No secret access key given"
|
25
|
+
@use_ssl = options[:use_ssl]
|
26
|
+
@timeout = options[:timeout]
|
27
|
+
@debug = options[:debug]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns all queues in the service and caches the result (see reload)
|
31
|
+
def queues(reload = false)
|
32
|
+
if reload or @queues.nil?
|
33
|
+
@queues = list_queues
|
34
|
+
else
|
35
|
+
@queues
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
proxy :queues do
|
40
|
+
# Builds new queue with given name
|
41
|
+
def create(name, default_visibility_timeout = nil)
|
42
|
+
options = { "QueueName" => name }
|
43
|
+
options["DefaultVisibilityTimeout"] = default_visibility_timeout.to_s if default_visibility_timeout
|
44
|
+
proxy_owner.send(:create_queue, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Finds the queue with given name
|
48
|
+
def find_first(name)
|
49
|
+
find_all(name).first
|
50
|
+
end
|
51
|
+
alias :find :find_first
|
52
|
+
|
53
|
+
# Find all queues in the service
|
54
|
+
def find_all(name)
|
55
|
+
if name and not name.empty?
|
56
|
+
proxy_owner.send(:list_queues, "QueueNamePrefix" => name)
|
57
|
+
else
|
58
|
+
proxy_target
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Reloads the queue list (clears the cache)
|
63
|
+
def reload
|
64
|
+
proxy_owner.queues(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Destroy all queues in the service (USE WITH CARE!).
|
68
|
+
def destroy_all
|
69
|
+
proxy_target.each { |queue| queue.destroy }
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect #:nodoc:
|
75
|
+
"#<#{self.class}:#@access_key_id>"
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def list_queues(options = {})
|
81
|
+
response = service_request(options.merge("Action" => "ListQueues"))
|
82
|
+
|
83
|
+
parse_list_queues_result(response.body).map do |url|
|
84
|
+
Queue.send(:new, self, url)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_queue(options)
|
89
|
+
name = options["QueueName"]
|
90
|
+
raise ArgumentError, "Invalid queue name: #{name}" unless queue_name_valid?(name)
|
91
|
+
|
92
|
+
response = service_request(options.merge("Action" => "CreateQueue"))
|
93
|
+
|
94
|
+
url = parse_create_queue_result(response.body)
|
95
|
+
Queue.send(:new, self, url)
|
96
|
+
end
|
97
|
+
|
98
|
+
def queue_name_valid?(name)
|
99
|
+
name =~ /\A[a-zA-Z0-9_-]{1,80}\Z/
|
100
|
+
end
|
101
|
+
|
102
|
+
def service_request(options = {})
|
103
|
+
connection.request(options.merge(:path => "/#{options[:path]}"))
|
104
|
+
end
|
105
|
+
|
106
|
+
def connection
|
107
|
+
if @connection.nil?
|
108
|
+
@connection = Connection.new
|
109
|
+
@connection.access_key_id = @access_key_id
|
110
|
+
@connection.secret_access_key = @secret_access_key
|
111
|
+
@connection.use_ssl = @use_ssl
|
112
|
+
@connection.timeout = @timeout
|
113
|
+
@connection.debug = @debug
|
114
|
+
end
|
115
|
+
@connection
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Sqs
|
2
|
+
|
3
|
+
# Class responsible for generating signatures to requests.
|
4
|
+
#
|
5
|
+
# Implements algorithm defined by Amazon Web Services to sign
|
6
|
+
# request with secret private credentials
|
7
|
+
|
8
|
+
class Signature
|
9
|
+
UNSAFE_REGEXP = /[^a-zA-Z0-9_.-]/
|
10
|
+
|
11
|
+
# Generates signature for given parameters
|
12
|
+
#
|
13
|
+
# ==== Parameters:
|
14
|
+
# +options+: a hash that contains options listed below
|
15
|
+
#
|
16
|
+
# ==== Options:
|
17
|
+
# +host+: hostname
|
18
|
+
# +access_key_id+: access key id
|
19
|
+
# +secret_access_key+: secret access key
|
20
|
+
# +method+: method of the request ("GET" or "POST")
|
21
|
+
# +params+: request parameters hash
|
22
|
+
# +path+: request path
|
23
|
+
#
|
24
|
+
# ==== Returns:
|
25
|
+
# Generated signature for given hostname and request
|
26
|
+
def self.generate(options)
|
27
|
+
host = options[:host]
|
28
|
+
access_key_id = options[:access_key_id]
|
29
|
+
secret_access_key = options[:secret_access_key]
|
30
|
+
method = options[:method]
|
31
|
+
params = options[:params]
|
32
|
+
path = options[:path]
|
33
|
+
|
34
|
+
string_to_sign = ""
|
35
|
+
string_to_sign << method.to_s.upcase
|
36
|
+
string_to_sign << "\n"
|
37
|
+
string_to_sign << host.to_s.downcase
|
38
|
+
string_to_sign << "\n"
|
39
|
+
string_to_sign << path
|
40
|
+
string_to_sign << "\n"
|
41
|
+
string_to_sign << canonicalized_query_string(params)
|
42
|
+
|
43
|
+
digest = OpenSSL::Digest::Digest.new("sha256")
|
44
|
+
hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string_to_sign)
|
45
|
+
base64 = Base64.encode64(hmac)
|
46
|
+
base64.chomp
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def self.canonicalized_query_string(params)
|
52
|
+
results = params.sort.map do |param|
|
53
|
+
urlencoded_key = URI.encode(param.first, UNSAFE_REGEXP)
|
54
|
+
urlencoded_value = URI.encode(param.last, UNSAFE_REGEXP)
|
55
|
+
"#{urlencoded_key}=#{urlencoded_value}"
|
56
|
+
end
|
57
|
+
results.join("&")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/sqs.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# stdlibs
|
2
|
+
require "base64"
|
3
|
+
require "forwardable"
|
4
|
+
require "net/http"
|
5
|
+
require "net/https"
|
6
|
+
require "openssl"
|
7
|
+
require "rexml/document"
|
8
|
+
require "time"
|
9
|
+
|
10
|
+
# proxy stuff
|
11
|
+
require "sqs/roxy/moxie"
|
12
|
+
require "sqs/roxy/proxy"
|
13
|
+
|
14
|
+
# sqs stuff
|
15
|
+
require "sqs/parser"
|
16
|
+
require "sqs/connection"
|
17
|
+
require "sqs/exceptions"
|
18
|
+
require "sqs/message"
|
19
|
+
require "sqs/queue"
|
20
|
+
require "sqs/service"
|
21
|
+
require "sqs/signature"
|
22
|
+
|
23
|
+
module Sqs
|
24
|
+
# Default (and only) host serving SQS stuff
|
25
|
+
HOST = "queue.amazonaws.com"
|
26
|
+
end
|
data/sqs.gemspec
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{sqs}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jakub Kuźma"]
|
12
|
+
s.date = %q{2009-08-17}
|
13
|
+
s.email = %q{qoobaa@gmail.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
".document",
|
19
|
+
".gitignore",
|
20
|
+
"LICENSE",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"lib/sqs.rb",
|
24
|
+
"lib/sqs/connection.rb",
|
25
|
+
"lib/sqs/exceptions.rb",
|
26
|
+
"lib/sqs/message.rb",
|
27
|
+
"lib/sqs/parser.rb",
|
28
|
+
"lib/sqs/queue.rb",
|
29
|
+
"lib/sqs/roxy/moxie.rb",
|
30
|
+
"lib/sqs/roxy/proxy.rb",
|
31
|
+
"lib/sqs/service.rb",
|
32
|
+
"lib/sqs/signature.rb",
|
33
|
+
"sqs.gemspec",
|
34
|
+
"test/signature_test.rb",
|
35
|
+
"test/test_helper.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/qoobaa/sqs}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.5}
|
41
|
+
s.summary = %q{Simple Queue Service accessing library}
|
42
|
+
s.test_files = [
|
43
|
+
"test/signature_test.rb",
|
44
|
+
"test/test_helper.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
49
|
+
s.specification_version = 3
|
50
|
+
|
51
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
52
|
+
else
|
53
|
+
end
|
54
|
+
else
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SignatureTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@options = {}
|
6
|
+
@params = {}
|
7
|
+
@options[:host] = "queue.amazonaws.com"
|
8
|
+
@options[:path] = "/"
|
9
|
+
@options[:method] = :get
|
10
|
+
@options[:access_key_id] = "0PN5J17HBGZHT7JJ3X82"
|
11
|
+
@options[:secret_access_key] = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o"
|
12
|
+
@params["SignatureMethod"] = "HmacSHA256"
|
13
|
+
@params["SignatureVersion"] = "2"
|
14
|
+
@params["Version"] = "2009-02-01"
|
15
|
+
@params["Timestamp"] = "2009-08-17T08:56:12Z"
|
16
|
+
@params["AWSAccessKeyId"] = @options[:access_key_id]
|
17
|
+
@options[:params] = @params
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_signature_for_get
|
21
|
+
@options[:method] = :get
|
22
|
+
expected = "lQcH/YUdHHpo4hshdHZrGbX9CLvksKmD9atmrCroyAo="
|
23
|
+
actual = Sqs::Signature.generate(@options)
|
24
|
+
assert_equal expected, actual
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_signagure_for_post
|
28
|
+
@options[:method] = :post
|
29
|
+
expected = "st9O4GTVytI+5BjbjfRSPRB8xKOxel52F7Sle706BcA="
|
30
|
+
actual = Sqs::Signature.generate(@options)
|
31
|
+
assert_equal expected, actual
|
32
|
+
end
|
33
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'rr'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
require 'sqs'
|
8
|
+
|
9
|
+
class Test::Unit::TestCase
|
10
|
+
include RR::Adapters::TestUnit
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sqs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Jakub Ku\xC5\xBAma"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-17 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: qoobaa@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
files:
|
25
|
+
- .document
|
26
|
+
- .gitignore
|
27
|
+
- LICENSE
|
28
|
+
- Rakefile
|
29
|
+
- VERSION
|
30
|
+
- lib/sqs.rb
|
31
|
+
- lib/sqs/connection.rb
|
32
|
+
- lib/sqs/exceptions.rb
|
33
|
+
- lib/sqs/message.rb
|
34
|
+
- lib/sqs/parser.rb
|
35
|
+
- lib/sqs/queue.rb
|
36
|
+
- lib/sqs/roxy/moxie.rb
|
37
|
+
- lib/sqs/roxy/proxy.rb
|
38
|
+
- lib/sqs/service.rb
|
39
|
+
- lib/sqs/signature.rb
|
40
|
+
- sqs.gemspec
|
41
|
+
- test/signature_test.rb
|
42
|
+
- test/test_helper.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/qoobaa/sqs
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --charset=UTF-8
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.5
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Simple Queue Service accessing library
|
71
|
+
test_files:
|
72
|
+
- test/signature_test.rb
|
73
|
+
- test/test_helper.rb
|