1""" 2The Client class, handling direct communication with the API 3""" 4 5from __future__ import print_function 6 7import base64 8import os 9import time 10import uuid 11from collections import OrderedDict 12 13from cryptography.hazmat.backends import default_backend 14from cryptography.hazmat.primitives import serialization 15from cryptography.hazmat.primitives import hashes 16from cryptography.hazmat.primitives.asymmetric import padding 17from suds.client import Client as SudsClient 18from suds.sudsobject import Object as SudsObject 19from suds.xsd.doctor import Import, ImportDoctor 20from suds.plugin import DocumentPlugin 21 22from . import __version__ 23 24try: 25 from urllib.parse import urlencode, quote_plus 26except ImportError: 27 from urllib import urlencode, quote_plus 28 29try: 30 import suds_requests 31except ImportError: 32 suds_requests = None 33 34 35URI_TEMPLATE = 'https://{}/wsdl/?service={}' 36 37MODE_RO = 'readonly' 38MODE_RW = 'readwrite' 39 40 41def convert_value(value): 42 """ 43 None and boolean values are not accepted by the Transip API. 44 This method converts 45 - None and False to an empty string, 46 - True to 1 47 """ 48 if isinstance(value, bool): 49 return 1 if value else '' 50 51 if not value: 52 return '' 53 54 return value 55 56 57class WSDLFixPlugin(DocumentPlugin): 58 # pylint: disable=W0232 59 """ 60 A SudsFilter to fix wsdl document before it is parsed. 61 """ 62 63 def loaded(self, context): 64 # pylint: disable=R0201 65 """ 66 Replaces an invalid type in the wsdl document with a validy type. 67 """ 68 context.document = context.document.replace(b'xsd:array', b'soapenc:Array') 69 70 71class Client(object): 72 # pylint: disable=R0205 73 """ 74 A client-base class, for other classes to base their service implementation 75 on. Contains methods to set and sign cookie and to retrieve the correct 76 WSDL for specific parts of the TransIP API. 77 78 Note: 79 You either need to supply a private_key or a private_key_file. 80 81 Args: 82 service_name (str): Name of the service. 83 login (str): The TransIP username. 84 private_key (str, optional): The content of the private key for 85 accessing the TransIP API. 86 private_key_file (str, optional): Path the the private key for 87 accesing the TransIP API. Defaults to 'decrypted_key'. 88 endpoint (str): The TransIP API endpoint. Defaults to 'api.transip.nl'. 89 """ 90 def __init__(self, service_name, login, private_key=None, 91 private_key_file='decrypted_key', endpoint='api.transip.nl'): 92 self.service_name = service_name 93 self.login = login 94 self.private_key = private_key 95 self.private_key_file = private_key_file 96 self.endpoint = endpoint 97 self.url = URI_TEMPLATE.format(endpoint, service_name) 98 99 imp = Import('http://schemas.xmlsoap.org/soap/encoding/') 100 doc = ImportDoctor(imp) 101 102 suds_kwargs = dict() 103 if suds_requests: 104 suds_kwargs['transport'] = suds_requests.RequestsTransport() 105 106 self.soap_client = SudsClient(self.url, doctor=doc, plugins=[WSDLFixPlugin()], **suds_kwargs) 107 108 def _sign(self, message): 109 """ Uses the decrypted private key to sign the message. """ 110 if self.private_key: 111 keydata = self.private_key 112 elif os.path.exists(self.private_key_file): 113 with open(self.private_key_file) as private_key: 114 keydata = private_key.read() 115 else: 116 raise RuntimeError('The private key does not exist.') 117 118 private_key = serialization.load_pem_private_key( 119 str.encode(keydata), 120 password=None, 121 backend=default_backend() 122 ) 123 signature = private_key.sign( 124 str.encode(message), 125 padding.PKCS1v15(), 126 hashes.SHA512(), 127 ) 128 129 signature = base64.b64encode(signature) 130 signature = quote_plus(signature) 131 132 return signature 133 134 def _build_signature_message(self, service_name, method_name, 135 timestamp, nonce, additional=None): 136 """ 137 Builds the message that should be signed. This message contains 138 specific information about the request in a specific order. 139 """ 140 if additional is None: 141 additional = [] 142 143 sign = OrderedDict() 144 # Add all additional parameters first 145 for index, value in enumerate(additional): 146 if isinstance(value, list): 147 for entryindex, entryvalue in enumerate(value): 148 if isinstance(entryvalue, SudsObject): 149 for objectkey, objectvalue in entryvalue: 150 objectvalue = convert_value(objectvalue) 151 sign[str(index) + '[' + str(entryindex) + '][' + objectkey + ']'] = objectvalue 152 elif isinstance(value, SudsObject): 153 for entryindex, entryvalue in value: 154 key = str(index) + '[' + str(entryindex) + ']' 155 sign[key] = convert_value(entryvalue) 156 else: 157 sign[index] = convert_value(value) 158 sign['__method'] = method_name 159 sign['__service'] = service_name 160 sign['__hostname'] = self.endpoint 161 sign['__timestamp'] = timestamp 162 sign['__nonce'] = nonce 163 164 return urlencode(sign) \ 165 .replace('%5B', '[') \ 166 .replace('%5D', ']') \ 167 .replace('+', '%20') \ 168 .replace('%7E', '~') # Comply with RFC3989. This replacement is also in TransIP's sample PHP library. 169 170 def update_cookie(self, cookies): 171 """ Updates the cookie for the upcoming call to the API. """ 172 temp = [] 173 for k, val in cookies.items(): 174 temp.append("%s=%s" % (k, val)) 175 176 cookiestring = ';'.join(temp) 177 self.soap_client.set_options(headers={'Cookie': cookiestring}) 178 179 def build_cookie(self, method, mode, parameters=None): 180 """ 181 Build a cookie for the request. 182 183 Keyword arguments: 184 method -- the method to be called on the service. 185 mode -- Read-only (MODE_RO) or read-write (MODE_RW) 186 """ 187 timestamp = int(time.time()) 188 nonce = str(uuid.uuid4())[:32] 189 190 message_to_sign = self._build_signature_message( 191 service_name=self.service_name, 192 method_name=method, 193 timestamp=timestamp, 194 nonce=nonce, 195 additional=parameters 196 ) 197 198 signature = self._sign(message_to_sign) 199 200 cookies = { 201 "nonce": nonce, 202 "timestamp": timestamp, 203 "mode": mode, 204 "clientVersion": __version__, 205 "login": self.login, 206 "signature": signature 207 } 208 209 return cookies 210 211 def _simple_request(self, method, *args, **kwargs): 212 """ 213 Helper method to create a request in a DRY way 214 """ 215 cookie = self.build_cookie(mode=kwargs.get('mode', MODE_RO), method=method, parameters=args) 216 self.update_cookie(cookie) 217 218 return getattr(self.soap_client.service, method)(*args) 219