context.py 6.76 KB
Newer Older
Michal Simon's avatar
Michal Simon committed
1
#   Copyright notice:
2
#   Copyright  Members of the EMI Collaboration, 2013.
3
#
Michal Simon's avatar
Michal Simon committed
4
#   See www.eu-emi.eu for details on the copyright holders
5
#
Michal Simon's avatar
Michal Simon committed
6
7
8
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
9
#
Michal Simon's avatar
Michal Simon committed
10
#       http://www.apache.org/licenses/LICENSE-2.0
11
#
Michal Simon's avatar
Michal Simon committed
12
13
14
15
16
17
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

18
19
from datetime import datetime
from M2Crypto import X509, RSA, EVP, BIO
20
21
22
23
try:
    from M2Crypto.ASN1 import UTC
except:
    from pytz import utc as UTC
24
import getpass
25
26
try:
    import simplejson as json
27
28
except:
    import json
ayllon's avatar
ayllon committed
29
30
31
import logging
import os
import sys
32
import urllib
ayllon's avatar
ayllon committed
33

34
from exceptions import *
35
from pycurlRequest import PycurlRequest
36
from request import Request
37

38
39
log = logging.getLogger(__name__)

ayllon's avatar
ayllon committed
40
41

# Return a list of certificates from the file
42
def _get_x509_list(cert):
43
44
    x509_list = []
    fd = BIO.openfile(cert, 'rb')
ayllon's avatar
ayllon committed
45
46
47
    cert = X509.load_cert_bio(fd)
    try:
        while True:
48
            x509_list.append(cert)
49
            log.debug("Loaded " + cert.get_subject().as_text())
ayllon's avatar
ayllon committed
50
51
52
53
            cert = X509.load_cert_bio(fd)
    except X509.X509Error:
        # When there are no more certs, this is what we get, so it is fine
        pass
54
55
56
57
    except BIO.BIOError:
        # When there are no more certs, this is what we get, so it is fine
        # Python 2.4
        pass
ayllon's avatar
ayllon committed
58

ayllon's avatar
ayllon committed
59
    del fd
60
    return x509_list
ayllon's avatar
ayllon committed
61
62


63
64
65
66
67
68
69
def _get_default_proxy():
    """
    Returns the default proxy location
    """
    return "/tmp/x509up_u%d" % os.geteuid()


ayllon's avatar
ayllon committed
70
class Context(object):
71

72
73
74
75
76
    def _read_passwd_from_stdin(self, *args, **kwargs):
        if not self.passwd:
            self.passwd = getpass.getpass('Private key password: ')
        return self.passwd

77
    def _set_x509(self, ucert, ukey):
78
79
80
81
82
83
        default_proxy_location = _get_default_proxy()

        # User certificate and key locations
        if ucert and not ukey:
            ukey = ucert
        elif not ucert:
ayllon's avatar
ayllon committed
84
            if 'X509_USER_PROXY' in os.environ:
85
86
87
                ukey = ucert = os.environ['X509_USER_PROXY']
            elif os.path.exists(default_proxy_location):
                ukey = ucert = default_proxy_location
ayllon's avatar
ayllon committed
88
89
            elif 'X509_USER_CERT' in os.environ:
                ucert = os.environ['X509_USER_CERT']
90
                ukey = os.environ.get('X509_USER_KEY', ucert)
91
92
93
            elif os.path.exists('/etc/grid-security/hostcert.pem') and os.path.exists('/etc/grid-security/hostkey.pem'):
                ucert = '/etc/grid-security/hostcert.pem'
                ukey = '/etc/grid-security/hostkey.pem'
ayllon's avatar
ayllon committed
94
95

        if ucert and ukey:
96
            self.x509_list = _get_x509_list(ucert)
97
98
            self.x509 = self.x509_list[0]
            not_after = self.x509.get_not_after()
99
            try:
100
                not_after = not_after.get_datetime()
101
102
103
104
105
106
107
108
109
            except:
                # Ugly hack for Python 2.4
                import time
                not_after = datetime.fromtimestamp(
                    time.mktime(time.strptime(str(not_after), "%b %d %H:%M:%S %Y %Z")),
                    tz=UTC
                )

            if not_after < datetime.now(UTC):
ayllon's avatar
ayllon committed
110
111
                raise Exception("Proxy expired!")

112
113
114
115
116
117
118
            try:
                self.rsa_key = RSA.load_key(ukey, self._read_passwd_from_stdin)
            except RSA.RSAError, e:
                raise RSA.RSAError("Could not load %s: %s" % (ukey, str(e)))
            except Exception, e:
                raise Exception("Could not load %s: %s" % (ukey, str(e)))

119
120
            self.evp_key = EVP.PKey()
            self.evp_key.assign_rsa(self.rsa_key)
ayllon's avatar
ayllon committed
121
122
123

            self.ucert = ucert
            self.ukey = ukey
124
125
        else:
            self.ucert = self.ukey = None
ayllon's avatar
ayllon committed
126

127
        if not self.ucert and not self.ukey:
128
            log.warning("No user certificate given!")
129
        else:
130
131
            log.debug("User certificate: %s" % self.ucert)
            log.debug("User private key: %s" % self.ukey)
132

133
    def _set_endpoint(self, endpoint):
ayllon's avatar
ayllon committed
134
135
136
137
        self.endpoint = endpoint
        if self.endpoint.endswith('/'):
            self.endpoint = self.endpoint[:-1]

138
    def _validate_endpoint(self):
ayllon's avatar
ayllon committed
139
        try:
140
141
            endpoint_info = json.loads(self.get('/'))
            endpoint_info['url'] = self.endpoint
ayllon's avatar
ayllon committed
142
143
144
        except FTS3ClientException:
            raise
        except Exception, e:
145
            raise BadEndpoint("%s (%s)" % (self.endpoint, str(e))), None, sys.exc_info()[2]
146
        return endpoint_info
ayllon's avatar
ayllon committed
147

148
    def __init__(self, endpoint, ucert=None, ukey=None, verify=True, access_token=None, no_creds=False, capath=None,
149
                 request_class=PycurlRequest, connectTimeout=30, timeout=30):
150
151
        self.passwd = None

152
        self._set_endpoint(endpoint)
153
154
155
156
157
158
159
160
161
        if no_creds:
            self.ucert = self.ukey = self.access_token = None
        else:
            self.access_token = access_token
            if self.access_token:
                self.ucert = None
                self.ukey = None
            else:
                self._set_x509(ucert, ukey)
162
                
163
        self._requester = request_class(
164
165
166
        self.ucert, self.ukey, passwd=self.passwd, verify=verify, access_token=self.access_token, capath=capath,
            connectTimeout=connectTimeout, timeout=timeout)

167
        self.endpoint_info = self._validate_endpoint()
ayllon's avatar
ayllon committed
168
        # Log obtained information
169
170
        log.debug("Using endpoint: %s" % self.endpoint_info['url'])
        log.debug("REST API version: %(major)d.%(minor)d.%(patch)d" % self.endpoint_info['api'])
ayllon's avatar
ayllon committed
171

172
173
    def get_endpoint_info(self):
        return self.endpoint_info
ayllon's avatar
ayllon committed
174

175
176
177
178
    def get(self, path, args=None):
        if args:
            query = '&'.join(map(lambda (k, v): "%s=%s" % (k, urllib.quote(v)), args.iteritems()))
            path += '?' + query
ayllon's avatar
ayllon committed
179
180
181
182
183
184
185
186
187
188
189
190
191
        return self._requester.method('GET',
                                      "%s/%s" % (self.endpoint, path))

    def put(self, path, body):
        return self._requester.method('PUT',
                                      "%s/%s" % (self.endpoint, path),
                                      body)

    def delete(self, path):
        return self._requester.method('DELETE',
                                      "%s/%s" % (self.endpoint, path))

    def post_json(self, path, body):
192
193
        if not isinstance(body, str) and not isinstance(body, unicode):
            body = json.dumps(body)
ayllon's avatar
ayllon committed
194
195
196
        return self._requester.method('POST',
                                      "%s/%s" % (self.endpoint, path),
                                      body,
197
                                      headers={'Content-Type': 'application/json'})