~tribaal/txaws/xss-hardening

« back to all changes in this revision

Viewing changes to txaws/storage/client.py

  • Committer: Tristan Seligmann
  • Date: 2008-07-06 22:51:54 UTC
  • Revision ID: mithrandi@mithrandi.net-20080706225154-35ok5ivzh7h3ve59
Initial import of S3 code from EdgeVerse.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Client wrapper for Amazon's Simple Storage Service.
 
3
 
 
4
API stability: unstable.
 
5
 
 
6
Various API-incompatible changes are planned in order to expose missing
 
7
functionality in this wrapper.
 
8
"""
 
9
 
 
10
 
 
11
import md5, hmac, sha
 
12
from base64 import b64encode
 
13
 
 
14
try:
 
15
    from xml.etree.ElementTree import XML
 
16
except ImportError:
 
17
    from elementtree.ElementTree import XML
 
18
 
 
19
from epsilon.extime import Time
 
20
 
 
21
from twisted.web.client import getPage
 
22
from twisted.web.http import datetimeToString
 
23
 
 
24
 
 
25
def calculateMD5(data):
 
26
    digest = md5.new(data).digest()
 
27
    return b64encode(digest)
 
28
 
 
29
 
 
30
def hmac_sha1(secret, data):
 
31
    digest = hmac.new(secret, data, sha).digest()
 
32
    return b64encode(digest)
 
33
 
 
34
 
 
35
class S3Request(object):
 
36
    def __init__(self, verb, bucket=None, objectName=None, data='',
 
37
            contentType=None, metadata={}, rootURI='https://s3.amazonaws.com',
 
38
            accessKey=None, secretKey=None):
 
39
        self.verb = verb
 
40
        self.bucket = bucket
 
41
        self.objectName = objectName
 
42
        self.data = data
 
43
        self.contentType = contentType
 
44
        self.metadata = metadata
 
45
        self.rootURI = rootURI
 
46
        self.accessKey = accessKey
 
47
        self.secretKey = secretKey
 
48
        self.date = datetimeToString()
 
49
 
 
50
        if (accessKey is not None and secretKey is None) or (accessKey is None and secretKey is not None):
 
51
            raise ValueError('Must provide both accessKey and secretKey, or neither')
 
52
 
 
53
    def getURIPath(self):
 
54
        path = '/'
 
55
        if self.bucket is not None:
 
56
            path += self.bucket
 
57
            if self.objectName is not None:
 
58
                path += '/' + self.objectName
 
59
        return path
 
60
 
 
61
    def getURI(self):
 
62
        return self.rootURI + self.getURIPath()
 
63
 
 
64
    def getHeaders(self):
 
65
        headers = {'Content-Length': len(self.data),
 
66
                   'Content-MD5': calculateMD5(self.data),
 
67
                   'Date': self.date}
 
68
 
 
69
        for key, value in self.metadata.iteritems():
 
70
            headers['x-amz-meta-' + key] = value
 
71
 
 
72
        if self.contentType is not None:
 
73
            headers['Content-Type'] = self.contentType
 
74
 
 
75
        if self.accessKey is not None:
 
76
            signature = self.getSignature(headers)
 
77
            headers['Authorization'] = 'AWS %s:%s' % (self.accessKey, signature)
 
78
 
 
79
        return headers
 
80
 
 
81
    def getCanonicalizedResource(self):
 
82
        return self.getURIPath()
 
83
 
 
84
    def getCanonicalizedAmzHeaders(self, headers):
 
85
        result = ''
 
86
        headers = [(name.lower(), value) for name, value in headers.iteritems() if name.lower().startswith('x-amz-')]
 
87
        headers.sort()
 
88
        return ''.join('%s:%s\n' % (name, value) for name, value in headers)
 
89
 
 
90
    def getSignature(self, headers):
 
91
        text = self.verb + '\n'
 
92
        text += headers.get('Content-MD5', '') + '\n'
 
93
        text += headers.get('Content-Type', '') + '\n'
 
94
        text += headers.get('Date', '') + '\n'
 
95
        text += self.getCanonicalizedAmzHeaders(headers)
 
96
        text += self.getCanonicalizedResource()
 
97
        return hmac_sha1(self.secretKey, text)
 
98
 
 
99
    def submit(self):
 
100
        return self.getPage(url=self.getURI(), method=self.verb, postdata=self.data, headers=self.getHeaders())
 
101
 
 
102
    def getPage(self, *a, **kw):
 
103
        return getPage(*a, **kw)
 
104
 
 
105
 
 
106
NS = '{http://s3.amazonaws.com/doc/2006-03-01/}'
 
107
 
 
108
class S3(object):
 
109
    rootURI = 'https://s3.amazonaws.com/'
 
110
    requestFactory = S3Request
 
111
 
 
112
    def __init__(self, accessKey, secretKey):
 
113
        self.accessKey = accessKey
 
114
        self.secretKey = secretKey
 
115
 
 
116
    def makeRequest(self, *a, **kw):
 
117
        """
 
118
        Create a request with the arguments passed in.
 
119
 
 
120
        This uses the requestFactory attribute, adding the credentials to the
 
121
        arguments passed in.
 
122
        """
 
123
        return self.requestFactory(accessKey=self.accessKey, secretKey=self.secretKey, *a, **kw)
 
124
 
 
125
    def _parseBucketList(self, response):
 
126
        """
 
127
        Parse XML bucket list response.
 
128
        """
 
129
        root = XML(response)
 
130
        for bucket in root.find(NS + 'Buckets'):
 
131
            yield {'name': bucket.findtext(NS + 'Name'),
 
132
                   'created': Time.fromISO8601TimeAndDate(bucket.findtext(NS + 'CreationDate'))}
 
133
 
 
134
    def listBuckets(self):
 
135
        """
 
136
        List all buckets.
 
137
 
 
138
        Returns a list of all the buckets owned by the authenticated sender of
 
139
        the request.
 
140
        """
 
141
        return self.makeRequest('GET').submit().addCallback(self._parseBucketList)
 
142
 
 
143
    def createBucket(self, bucket):
 
144
        """
 
145
        Create a new bucket.
 
146
        """
 
147
        return self.makeRequest('PUT', bucket).submit()
 
148
 
 
149
    def deleteBucket(self, bucket):
 
150
        """
 
151
        Delete a bucket.
 
152
 
 
153
        The bucket must be empty before it can be deleted.
 
154
        """
 
155
        return self.makeRequest('DELETE', bucket).submit()
 
156
 
 
157
    def putObject(self, bucket, objectName, data, contentType=None, metadata={}):
 
158
        """
 
159
        Put an object in a bucket.
 
160
 
 
161
        Any existing object of the same name will be replaced.
 
162
        """
 
163
        return self.makeRequest('PUT', bucket, objectName, data, contentType, metadata)
 
164
 
 
165
    def getObject(self, bucket, objectName):
 
166
        """
 
167
        Get an object from a bucket.
 
168
        """
 
169
        return self.makeRequest('GET', bucket, objectName)
 
170
 
 
171
    def headObject(self, bucket, objectName):
 
172
        """
 
173
        Retrieve object metadata only.
 
174
 
 
175
        This is like getObject, but the object's content is not retrieved.
 
176
        Currently the metadata is not returned to the caller either, so this
 
177
        method is mostly useless, and only provided for completeness.
 
178
        """
 
179
        return self.makeRequest('HEAD', bucket, objectName)
 
180
 
 
181
    def deleteObject(self, bucket, objectName):
 
182
        """
 
183
        Delete an object from a bucket.
 
184
 
 
185
        Once deleted, there is no method to restore or undelete an object.
 
186
        """
 
187
        return self.makeRequest('DELETE', bucket, objectName)