Package expedient :: Package common :: Package xmlrpc_serverproxy :: Module models
[hide private]
[frames] | no frames]

Source Code for Module expedient.common.xmlrpc_serverproxy.models

  1  ''' 
  2  Created on Apr 30, 2010 
  3   
  4  @author: jnaous 
  5  ''' 
  6   
  7  from django.db import models 
  8  import os 
  9  import binascii 
 10  from django.conf import settings 
 11  from datetime import timedelta, datetime 
 12  import time 
 13  from expedient.common.utils.transport import TestClientTransport 
 14  from urlparse import urlparse 
 15  from django.contrib.auth.models import User 
 16  import xmlrpclib 
 17  from expedient.common.tests.utils import test_to_http 
 18   
19 -def get_max_password_len():
20 # M2Crypto does not like it when header fields are large. Creates bad 21 # requests. So limit to 40 hex characters = 20 bytes = 160 bits. 22 return 40
23
24 -def random_password():
25 return binascii.b2a_hex(os.urandom(get_max_password_len()/2))
26
27 -def add_basic_auth(uri, username=None, password=None):
28 parsed = urlparse(uri.lower()) 29 if username: 30 if password: 31 new_url = "%s://%s:%s@%s%s" % (parsed.scheme, 32 username, 33 password, 34 parsed.netloc, 35 parsed.path) 36 else: 37 new_url = "%s://%s@%s%s" % (parsed.scheme, 38 username, 39 parsed.netloc, 40 parsed.path) 41 else: 42 new_url = uri 43 return new_url
44
45 -class BasicAuthServerProxy(xmlrpclib.ServerProxy):
46 - def __init__(self, uri, username=None, password=None, **kwargs):
47 new_url = add_basic_auth(uri, username, password) 48 49 # M2Crypto fails when using unicode URLs. 50 xmlrpclib.ServerProxy.__init__(self, str(new_url), **kwargs)
51
52 - def __repr__(self):
53 parsed = urlparse(self._ServerProxy__host) 54 new_url = "%s://%s%s" % (parsed.scheme, 55 parsed.netloc, 56 parsed.path) 57 return ( 58 "<ServerProxy for %s%s>" % 59 (new_url, self._ServerProxy__handler) 60 )
61 62 __str__ = __repr__
63
64 -class PasswordXMLRPCServerProxy(models.Model):
65 ''' 66 Implements a server proxy for XML-RPC calls that checks SSL certs and 67 uses a password to talk to the server. The password is automatically renewed 68 whenever it expires. 69 ''' 70 71 username = models.CharField( 72 max_length=100, 73 help_text="Username to use to access the remote server.") 74 password = models.CharField( 75 max_length=get_max_password_len(), 76 help_text="Password to use to access the remote server.") 77 max_password_age = models.PositiveIntegerField( 78 'Max Password age (days)', default=0, 79 help_text="If this is set to non-zero, the password "\ 80 "will automatically be changed once the password ages past the "\ 81 "maximum. The new password is then randomly generated.") 82 password_timestamp = models.DateTimeField(auto_now_add=True) 83 url = models.CharField("Server URL", max_length=1024) 84 85 verify_certs = models.BooleanField( 86 "Verify Certificates?", default=False, 87 help_text="Disabling this check will still verify that the "\ 88 "certificate itself is well-formed. In particular, the "\ 89 "server's hostname must match the certificate's Common Name.") 90
91 - def _reset_proxy(self):
92 parsed = urlparse(self.url.lower()) 93 self.transport = None 94 # This scheme is used for debugging and looping back 95 if parsed.scheme == "test": 96 self.proxy = BasicAuthServerProxy( 97 test_to_http(self.url), 98 username=self.username, 99 password=self.password, 100 transport=TestClientTransport()) 101 elif parsed.scheme == "https": 102 from M2Crypto.m2xmlrpclib import SSL_Transport 103 from M2Crypto.SSL import Context 104 self.transport = SSL_Transport(Context(protocol='tlsv1')) 105 self.proxy = BasicAuthServerProxy(self.url, 106 username=self.username, 107 password=self.password, 108 transport=self.transport, 109 verbose=getattr(settings, "DEBUG", False)) 110 self.set_verify_certs() 111 else: 112 self.proxy = BasicAuthServerProxy(self.url, 113 username=self.username, 114 password=self.password)
115
116 - def _check_expiry(self):
117 # if the password has expired, it's time to set a new one 118 if self.password_timestamp and self.max_password_age: 119 max_age = timedelta(days=self.max_password_age) 120 expiry_time = self.password_timestamp + max_age 121 # normalize because django is screwy 122 expiry_time = time.mktime(expiry_time.timetuple()) 123 now = time.time() 124 if expiry_time <= now: 125 self.proxy.change_password(random_password())
126
127 - def __init__(self, *args, **kwargs):
128 super(PasswordXMLRPCServerProxy, self).__init__(*args, **kwargs) 129 if self.url: 130 self._reset_proxy() 131 self._check_expiry()
132 133 # def __getattr__(self, name): 134 # if name == "proxy": 135 # raise AttributeError("Attribute 'proxy' not found.") 136 # return getattr(self.proxy, name) 137
138 - def save(self, *args, **kwargs):
139 super(PasswordXMLRPCServerProxy, self).save(*args, **kwargs) 140 if self.url: 141 self._reset_proxy() 142 self.set_verify_certs()
143
144 - def set_verify_certs(self):
145 '''Enable/disable SSL certificate verification.''' 146 if self.transport: 147 from M2Crypto import SSL 148 if self.verify_certs: 149 self.transport.ssl_ctx.set_verify( 150 SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 16) 151 self.transport.ssl_ctx.load_verify_locations( 152 capath=settings.XMLRPC_TRUSTED_CA_PATH) 153 else: 154 self.transport.ssl_ctx.set_verify( 155 SSL.verify_none, 1)
156
157 - def change_password(self, password=None):
158 '''Change the remote password''' 159 password = password or random_password() 160 self.password_timestamp = datetime.today() 161 err = self.__getattr__('change_password')(password) 162 if not err: 163 # password change succeeded 164 self.password = password 165 self._reset_proxy() 166 self.save() 167 else: 168 print "Error changing password: %s" % err 169 return err
170
171 - def ping(self, data):
172 return self.proxy.ping(data)
173
174 - def is_available(self, get_info=False):
175 '''Call the server's ping method, and see if we get a pong''' 176 try: 177 ret = self.ping("PING") 178 except Exception as e: 179 import traceback 180 print "Exception while pinging server: %s" % e 181 traceback.print_exc() 182 if get_info: 183 return (False, str(e)) 184 else: 185 return False 186 187 if "PING" in ret and "PONG" in ret: 188 if get_info: 189 return (True, None) 190 else: 191 return True 192 else: 193 msg = "Server at %s returned unexpected data %s" % (self.url, ret) 194 print msg 195 if get_info: 196 return (False, msg) 197 else: 198 return False
199
200 - def install_trusted_ca(self):
201 ''' 202 Add the CA that signed the certificate for self.url as trusted. 203 ''' 204 import ssl 205 import subprocess 206 207 # parse the url 208 res = urlparse(self.url) 209 if res.scheme.lower() != "https": 210 return 211 212 port = res.port or 443 213 214 # get the PEM-encoded certificate 215 cert = ssl.get_server_certificate((res.hostname, port)) 216 217 # the returned cert maybe messed up because of python-ssl bug Issue8086 218 if not cert.endswith("\n-----END CERTIFICATE-----\n"): 219 cert = cert.replace("-----END CERTIFICATE-----", 220 "\n-----END CERTIFICATE-----\n") 221 222 # dump it in the directory, and run make 223 with open(os.path.join(settings.XMLRPC_TRUSTED_CA_PATH, 224 res.hostname+"-ca.crt"), 225 'w') as cert_file: 226 cert_file.write(cert) 227 228 # TODO: Don't run make here. Do the linking manually. 229 subprocess.Popen(['make', '-C', settings.XMLRPC_TRUSTED_CA_PATH], 230 stdin=subprocess.PIPE, 231 stdout=subprocess.PIPE, 232 stderr=subprocess.PIPE, 233 )
234