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