-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimple_aws.rb
More file actions
157 lines (141 loc) · 5.55 KB
/
Copy pathsimple_aws.rb
File metadata and controls
157 lines (141 loc) · 5.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
require 'time'
require 'openssl'
require 'net/http'
# Pure-ruby implementation of common AWS HTTP requests
# Note that OpenSSL must be installed on the system
# Works for Ruby >= 2.0 (minor differences in Net::HTTP::Get etc arguments
# prevent this from working in Ruby 1.9.x)
module SimpleAws
# Usage:
# s3 = SimpleAws::S3.new('example-bucket')
# s3.put('example.txt', 'this is an example')
# s3.get('example.txt') => 'this is an example'
# s3.list('ex') => {
# [{key: 'example.txt', ...}, ...]
class S3
def initialize(bucket, opts = {})
@bucket = bucket
opts[:service] = 's3'
@sig = AwsSigV4.new(opts)
end
def get(key)
path = File.join('/', key)
uri = URI.parse("https://#{@bucket}.s3.amazonaws.com#{path}")
req = Net::HTTP::Get.new(uri)
@sig.signed_headers(:get, uri, {}).each {|k, v| req[k] = v}
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
raise "Invalid response from AWS: #{res.code}" unless res.code.to_s == '200'
res.body
end
def put(key, content)
path = File.join('/', key)
uri = URI.parse("https://#{@bucket}.s3.amazonaws.com#{path}")
req = Net::HTTP::Put.new(uri)
req.body = content
headers = {'Content-Type' => 'binary/octet-stream'}
@sig.signed_headers(:put, uri, headers, content).each {|k, v| req[k] = v}
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
raise "Invalid response from AWS: #{res.code}" unless res.code.to_s == '200'
puts res.inspect
res.body
end
def list(prefix)
prefix = prefix[1..-1] if prefix[0] == '/'
uri = URI.parse("https://#{@bucket}.s3.amazonaws.com/?list-type=2&prefix=#{prefix}")
req = Net::HTTP::Get.new(uri)
@sig.signed_headers(:get, uri, {}).each {|k, v| req[k] = v}
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
raise "Invalid response from AWS: #{res.code}" unless res.code.to_s == '200'
contents = res.body
contents = contents.gsub(/<Contents>/, "\n<Contents>").split("\n")
contents = contents.select{|cl| cl =~ /^<Contents>/}
contents.map do |cl|
{ key: (/<Key>(.*)<\/Key>/ =~ cl) ? $1 : nil,
last_modified: (/<LastModified>(.*)<\/LastModified>/ =~ cl) ? $1 : nil,
etag: (/<ETag>(.*)<\/ETag>/ =~ cl) ? $1.gsub(/"/,'') : nil,
size: (/<Size>(.*)<\/Size>/ =~ cl) ? $1 : nil,
storage_class: (/<StorageClass>(.*)<\/StorageClass>/ =~ cl) ? $1 : nil,
}
end
end
end
class AwsSigV4
def initialize(opts = {})
@service = opts[:service]
@region = opts[:region] || 'us-east-1'
@key_id = opts[:key_id] || ENV['AWS_ACCESS_KEY_ID']
@secret = opts[:secret] || ENV['AWS_SECRET_ACCESS_KEY']
raise 'Must provide :service' unless @service
raise 'Must provide :key_id or set ENV[AWS_ACCESS_KEY_ID]' unless @key_id
raise 'Must provide :secret or set ENV[AWS_SECRET_ACCESS_KEY]' unless @secret
end
def signed_headers(method, url, headers, payload = '')
can_req = canonical_request(method, url, headers, payload)
puts "#{'#'*40}\n#{can_req}\n#{'='*40}"
str_to_sign = string_to_sign(can_req, headers['x-amz-date'])
puts "#{'='*40}\n#{str_to_sign}\n#{'='*40}"
short_date = headers['x-amz-date'].split('T')[0]
key = signing_key(short_date)
sig = OpenSSL::HMAC.hexdigest('sha256', key, str_to_sign)
signed_headers = headers.keys.map{|h| h.downcase}.sort.join(';')
auth_str = auth_string(signed_headers, short_date, sig)
headers['Authorization'] = auth_str
headers
end
private
def canonical_request(method, uri, headers, payload = '')
uri = URI.parse(uri) unless uri.is_a? URI
can_uri = "#{uri.path.empty? ? '/' : uri.path}"
can_query = URI.encode(uri.query || '')
# add content sha to the headers
payload_hash = Digest::SHA256.hexdigest(payload)
headers['x-amz-content-sha256'] = payload_hash
headers['Host'] = uri.host
# add the date to the headers
request_time = Time.now
headers['Date'] ||= request_time.httpdate
headers['x-amz-date'] ||= request_time.utc.strftime("%Y%m%dT%H%M%SZ")
can_headers = canonical_headers(headers)
signed_headers = headers.keys.map{|h| h.downcase}
signed_headers = signed_headers.sort.join(';')
[ method.to_s.upcase,
can_uri,
can_query,
can_headers,
signed_headers,
payload_hash
].join("\n")
end
def string_to_sign(canonical_request, date)
[ 'AWS4-HMAC-SHA256',
date,
"#{date.split('T')[0]}/#{@region}/#{@service}/aws4_request",
Digest::SHA256.hexdigest(canonical_request)
].join("\n")
end
def signing_key(date)
key = OpenSSL::HMAC.digest('sha256', "AWS4#{@secret}", date)
key = OpenSSL::HMAC.digest('sha256', key, @region)
key = OpenSSL::HMAC.digest('sha256', key, @service)
OpenSSL::HMAC.digest('sha256', key, 'aws4_request')
end
def auth_string(signed_headers, short_date, signature)
pieces = [
"Credential=#{@key_id}/#{short_date}/#{@region}/#{@service}/aws4_request",
"SignedHeaders=#{signed_headers}",
"Signature=#{signature}"
]
"AWS4-HMAC-SHA256 #{pieces.join(',')}"
end
def canonical_headers(headers)
can_hdr = headers.keys.map {|h| "#{h.downcase}:#{headers[h].strip}"}.sort
can_hdr.join("\n") + "\n"
end
end
end