def _init_message_id(self, message_mdn): if is_none_or_whitespace(message_mdn.mdn_message_id): raise ValueError("mdn-message-id is required") if is_none_or_whitespace(message_mdn.original_message_id): raise ValueError("original-message-id is required") self.mdn_message_id = message_mdn.mdn_message_id self.original_message_id = message_mdn.original_message_id
def _init_mic(self, is_mic, message_mdn): if not isinstance(is_mic, bool): raise ValueError("mdn mic flag invalid") if is_mic and (is_none_or_whitespace(message_mdn.mdn_mic_digest) or is_none_or_whitespace(message_mdn.mdn_mic_algorithm)): raise ValueError("mdn mic digest or algorithm invalid") self.is_mic = is_mic self.mic_algorithm = message_mdn.mdn_mic_algorithm self.mic_digest = message_mdn.mdn_mic_digest self.mic_description = None
def _build_feedback_content_mime(self): mdn_feedback_content = email.Message.Message() mdn_feedback_content.set_type('message/disposition-notification') mdn_lines = [] mdn_lines.append('Reporting-UA: {title}/{version}'.format( title=ngas2.__title__, version=ngas2.__version__)) mdn_lines.append('Original-Recipient: rfc822; {local_identity}'.format( local_identity=self.context.agreement.local_identity)) mdn_lines.append('Final-Recipient: rfc822; {local_identity}'.format( local_identity=self.context.agreement.local_identity)) mdn_lines.append('Original-Message-ID: {message_id}'.format( message_id=self.original_message_id)) disposition_description = 'Disposition: {mode}; {type}'.format( mode=self.disposition_mode, type=self.disposition_type) if not is_none_or_whitespace(self.disposition_modifier_code): disposition_description += '/{code}'.format( code=self.disposition_modifier_code) if not is_none_or_whitespace(self.disposition_modifier_value): disposition_description += ': {value}'.format( value=self.disposition_modifier_value) self.disposition_description = disposition_description mdn_lines.append(self.disposition_description) if self.disposition_type == 'processed' and is_none_or_whitespace( self.disposition_modifier_code): self.validate_status = StatusType.successful else: self.validate_status = StatusType.failed if self.is_mic: self.mic_description = 'Received-content-MIC: {digest}, {algorithm}'.format( digest=self.mic_digest, algorithm=self.mic_algorithm) mdn_lines.append(self.mic_description) mdn_feedback_content.set_payload('\r\n'.join(mdn_lines) + '\r\n') del mdn_feedback_content['MIME-Version'] self.context.trace('build mdn processed result information') return mdn_feedback_content
def _verify_signed_message(self, content_type): if content_type != 'multipart/signed': raise AS2MdnException('content-type invalid when signed required') mdn_signature_thumbprint = self.context.agreement.outbound_agreement.message_encryption_certificate.thumbprint mdn_signature_cert_path = self.context.agreement.outbound_agreement.message_encryption_certificate.local_file_path mdn_signature_cert_ca_path = self.context.agreement.outbound_agreement.message_encryption_certificate.local_ca_file_path mdn_signature_cert_verify = self.context.agreement.outbound_agreement.message_encryption_certificate.is_need_verify try: if is_none_or_whitespace(mdn_signature_cert_ca_path): mdn_signature_cert_ca_path = mdn_signature_cert_path f_mime_string = SMIMEHelper.format_with_cr_lf( SMIMEHelper.mime_to_string(self.mime_message)) SMIMEHelper.verify_signed_text(f_mime_string, mdn_signature_cert_path, mdn_signature_cert_ca_path, mdn_signature_cert_verify) self.context.trace( "mdn signature verify finished; thumbprint: {thumbprint}, verify certificate: {verify}", thumbprint=mdn_signature_thumbprint, verify=mdn_signature_cert_verify) except: logger.exception( "mdn signature verify failed; mdn-message-id: {id}".format( id=self.mdn_message_id)) raise AS2MdnException( "mdn signature verify failed; thumbprint: {thumbprint}, verify certificate: {verify}, due to: {message}", thumbprint=mdn_signature_thumbprint, verify=mdn_signature_cert_verify, message=sys.exc_info()[1])
def _decode_disposition(self, mdn): disposition_description = mdn.get('Disposition') self.disposition_description = disposition_description if is_none_or_whitespace(disposition_description): raise AS2MdnException('disposition is required') disposition = self.disposition_description.split(';') if len(disposition) != 2: raise AS2MdnException('disposition is invalid') self.disposition_mode = disposition[0].strip() disposition_result = disposition[1].strip().split('/') if len(disposition_result) == 1: self.disposition_type = disposition_result[0].strip() elif len(disposition_result) == 2: self.disposition_type = disposition_result[0].strip() disposition_modifier = disposition_result[1].split(':') if len(disposition_modifier) == 1: self.disposition_modifier_code = disposition_modifier[0].strip( ) elif len(disposition_modifier) == 2: self.disposition_modifier_code = disposition_modifier[0].strip( ) self.disposition_modifier_value = disposition_modifier[ 1].strip() else: raise AS2MdnException('disposition modifier is invalid') else: raise AS2MdnException('disposition type or modifier is invalid')
def _verify_signature(self): if not self.is_signed: self.context.trace("verify signature ignored") return content_type = self.mime_message.get_content_type().lower() if content_type != 'multipart/signed': raise AS2VerifySignatureException('verify signature failed; content-type:{type} invalid', type=content_type) cert_thumbprint = self.context.agreement.inbound_agreement.message_verify_certificate.thumbprint cert_local_file_path = self.context.agreement.inbound_agreement.message_verify_certificate.local_file_path cert_ca_local_file_path = self.context.agreement.inbound_agreement.message_verify_certificate.local_ca_file_path cert_verify = self.context.agreement.inbound_agreement.message_verify_certificate.is_need_verify try: if is_none_or_whitespace(cert_ca_local_file_path): cert_ca_local_file_path = cert_local_file_path for part in self.mime_message.get_payload(): if not isinstance(part, email.message.Message): continue part_type = part.get_content_type().lower() part_encoding = part.get('Content-Transfer-Encoding', '').lower() if 'application/pkcs7-signature' == part_type and 'base64' != part_encoding: del part['Content-Transfer-Encoding'] email.encoders.encode_base64(part) self.context.trace('signature content transfer encoding to base64') f_mime_string = SMIMEHelper.format_with_cr_lf( SMIMEHelper.mime_to_string(self.mime_message)) SMIMEHelper.verify_signed_text( f_mime_string, cert_local_file_path, cert_ca_local_file_path, cert_verify) self.mic_algorithm = self.mime_message.get_param('micalg').lower() parts = [part for part in self.mime_message.walk() if part.get_content_type() not in ['multipart/signed', 'application/pkcs7-signature']] if len(parts) != 1: raise AS2VerifySignatureException( "verify signature failed; due to multiple part content in mime message") self.mime_message = parts[0] self.context.trace("verify signature finished; thumbprint: {thumbprint}, verify certificate: {verify}", thumbprint=cert_thumbprint, verify=cert_verify) except: logger.exception("verify signature failed; message-id: {id}".format(id=self.message_id)) raise AS2VerifySignatureException( "verify signature failed; thumbprint: {thumbprint}, verify certificate: {verify}, due to: {message}", thumbprint=cert_thumbprint, verify=cert_verify, message=sys.exc_info()[1])
def _get_send_data(self, data_dfs_path): data = self.body if not is_none_or_whitespace(data_dfs_path): data = DFSHelper.download(data_dfs_path) if data is None: raise Exception("get send data from dfs failed") return data
def _get_send_request(self): headers = { k.lower().replace('_', '-'): v for k, v in self.headers.items() } business_id = headers.get(self.ngas2_business_id) agreement_id = headers.get(self.ngas2_agreement_id) data_dfs_path = headers.get(self.ngas2_data_dfs_path) if is_none_or_whitespace(agreement_id): raise Exception("as2 agreement id is required") if is_none_or_whitespace(data_dfs_path) and is_none_or_whitespace( self.body): raise Exception("as2 data is required") return business_id, agreement_id, data_dfs_path
def _init_message_id(self): id = self.headers.get("message-id") if is_none_or_whitespace(id): raise AS2Exception('message-id is required') if len(id) > 998: raise AS2Exception('message-id length is a maximum of 998 characters') self.message_id = id.strip()
def _decode_original_message_id(self, mdn): original_message_id = mdn.get('Original-Message-ID') if is_none_or_whitespace(original_message_id): raise AS2MdnException('original-message-id is required') if len(original_message_id) > 998: raise AS2MdnException( 'original-message-id length is a maximum of 998 characters') self.original_message_id = original_message_id
def _init_mdn_message_id(self): msg_id = self.headers.get("message-id", str(uuid.uuid4()).upper()) if is_none_or_whitespace(msg_id): raise AS2MdnException('mdn-message-id is required') if len(msg_id) > 998: raise AS2MdnException( 'mdn-message-id length is a maximum of 998 characters') self.mdn_message_id = msg_id.strip()
def _get_primary_agreement(self, local_identity, trading_identity): if is_none_or_whitespace(trading_identity): raise Exception("as2-from is required") if is_none_or_whitespace(local_identity): raise Exception("as2-to is required") cache = InMemoryCache('PartnerManager', 'Agreement') cache_key = local_identity + '_' + trading_identity cache_value = cache[cache_key] if cache_value is not None: return cache_value try: condition = { 'LocalIdentity': local_identity, 'TradingIdentity': trading_identity, 'IsPrimary': True } docs = CloudStoreHelper().find_documents(condition, Agreement) if docs is None or docs.records is None or len(docs.records) != 1: raise Exception("agreement is null or length invalid") agreement = docs.records[0] if agreement.status != AgreementStatus.active: raise Exception("agreement status invalid") except: raise Exception("get as2 agreement failed; due to {msg}".format( msg=sys.exc_info()[1])) self._build_outbound_agreement_certificate( agreement.outbound_agreement) self._build_inbound_agreement_certificate(agreement.inbound_agreement) cache[cache_key] = agreement return agreement
def _decode_received_content_mic(self, mdn): received_content_mic_description = mdn.get('Received-Content-MIC') self.mic_description = received_content_mic_description if is_none_or_whitespace(received_content_mic_description): return mic = self.mic_description.split(',') if len(mic) != 2: raise AS2MdnException('received-content-mic is invalid') self.mic_digest = mic[0].strip() self.mic_algorithm = mic[1].strip()
def _fetch_content(self): charset = self.mime_message.get_charset() content_type = self.mime_message.get_content_type() content = self.mime_message.get_payload(decode=True) self.content = content self.content_length = len(content) self.content_hash = hashlib.md5(content).hexdigest() if not is_none_or_whitespace(content_type): self.content_type = content_type.lower() self.content_charset = None if charset is None else str(charset) self.context.trace("fetch content finished")
def __init__(self, message_id, data_feed_name, data_feed_content, context): if not isinstance(context, AS2Context): raise ValueError('context invalid') if is_none_or_whitespace(message_id): raise ValueError('message id is null or empty') if is_none_or_whitespace(data_feed_name): self.data_feed_name = ngas2.__title__ else: self.data_feed_name = data_feed_name self.data_feed_content = data_feed_content self.context = context self.message_id = message_id self.headers = None self.body = None self.mime_message = None self.is_mic = False self.mic_content = None self.mic_digest = None self.mic_algorithm = None self.mic_description = None
def _build_confirmation_mime(self): confirmation_text = self.context.agreement.inbound_agreement.mdn_confirmation_text if is_none_or_whitespace(confirmation_text): confirmation_text = "received by {title}/{version}".format( title=ngas2.__title__, version=ngas2.__version__) mdn_confirmation_text = email.Message.Message() mdn_confirmation_text.set_payload( "{confirmation_text}".format(confirmation_text=confirmation_text)) mdn_confirmation_text.set_type('text/plain') del mdn_confirmation_text['MIME-Version'] self.context.trace('build mdn confirmation information') return mdn_confirmation_text
def receive_async_mdn(self, agreement_id=None): if is_none_or_whitespace(agreement_id): headers = { k.lower().replace('_', '-'): v for k, v in self.headers.items() } trading_identity = decode_as2_identity(headers.get('as2-from', '')) local_identity = decode_as2_identity(headers.get('as2-to', '')) agreement = self._get_primary_agreement(local_identity, trading_identity) else: agreement = self._get_agreement(agreement_id) context = AS2Context(agreement, None) self._receive_async_mdn(self.headers, self.body, context)
def _init_disposition(self, message_mdn): if message_mdn.mdn_disposition_mode not in [ 'automatic-action/MDN-sent-automatically', 'manual-action/MDN-sent-manually' ]: raise ValueError("mdn disposition mode invalid") if message_mdn.mdn_disposition_type not in ['processed', 'failed']: raise ValueError("mdn disposition type invalid") if not is_none_or_whitespace( message_mdn.mdn_disposition_modifier_code) \ and message_mdn.mdn_disposition_modifier_code not in ['error', 'warning', 'failure']: raise ValueError("mdn disposition modifier code invalid") self.disposition_mode = message_mdn.mdn_disposition_mode self.disposition_type = message_mdn.mdn_disposition_type self.disposition_modifier_code = message_mdn.mdn_disposition_modifier_code self.disposition_modifier_value = message_mdn.mdn_disposition_modifier_value self.disposition_description = None
def _build_received_as2_context(self, id, message): if is_none_or_whitespace(id): headers = { k.lower().replace('_', '-'): v for k, v in self.headers.items() } trading_identity = decode_as2_identity(headers.get('as2-from', '')) local_identity = decode_as2_identity(headers.get('as2-to', '')) agreement = self._get_primary_agreement(local_identity, trading_identity) else: agreement = self._get_agreement(id) message.local_identity = agreement.local_identity message.trading_identity = agreement.trading_identity message.agreement_id = agreement.id return AS2Context(agreement, message)
def _build_certificate(self, cert_id): if cert_id is None: return None try: certificate = CloudStoreHelper().get_document(cert_id, Certificate) if certificate is None: raise Exception("as2 certificate is null") if certificate.type == CertificateType.public: certificate.local_file_path = os.path.join( self.conf['app_root_dir'], "cert", "public", certificate.thumbprint + ".pem") else: certificate.local_file_path = os.path.join( self.conf['app_root_dir'], "cert", "private", certificate.thumbprint + ".pem") if not is_none_or_whitespace(certificate.pass_phrase): certificate.pass_phrase = SMIMEHelper.aes_decrypt( certificate.pass_phrase, self.conf['app_secure_key']) if os.path.exists(certificate.local_file_path): return certificate local_path = os.path.dirname(certificate.local_file_path) if not os.path.exists(local_path): os.makedirs(local_path) if not DFSHelper.download2file(certificate.dfs_file_path, certificate.local_file_path): raise Exception("download certificate file from dfs failed") return certificate except: raise Exception( "get as2 certificate failed; certificate-id: {id}, due to {msg}" .format(id=cert_id, msg=sys.exc_info()[1]))
def _request_mdn(self): if not self.is_request_mdn: self.context.trace("request mdn ignored") return disposition_notification_to = self.context.agreement.outbound_agreement.disposition_notification_to is_mdn_signed = self.context.agreement.outbound_agreement.is_mdn_signed mic_alg_desc = '' mdn_mode = self.context.agreement.outbound_agreement.mdn_mode async_mdn_url = self.context.agreement.outbound_agreement.async_mdn_url self.headers['Disposition-Notification-To'] = disposition_notification_to if mdn_mode == MdnMode.async: self.headers['Receipt-Delivery-Option'] = async_mdn_url if is_mdn_signed: signed_receipt_protocol = 'signed-receipt-protocol=required,pkcs7-signature' signed_receipt_mic_alg_prefix = 'signed-receipt-micalg=' mdn_signature_algorithm = self.context.agreement.outbound_agreement.mdn_signature_algorithm if is_none_or_whitespace(mdn_signature_algorithm): mic_alg_desc = 'optional,{micalg}'.format( micalg=','.join(SignatureAlgorithm.all)) else: mic_alg_desc = 'required,{micalg}'.format( micalg=mdn_signature_algorithm) signed_receipt_mic_alg = signed_receipt_mic_alg_prefix + mic_alg_desc self.headers['Disposition-Notification-Options'] = signed_receipt_protocol + '; ' + signed_receipt_mic_alg self.context.trace("request mdn finished; mode: {mode}, signed: {signed}, algorithm: {algorithm}", mode=mdn_mode, signed=is_mdn_signed, algorithm=mic_alg_desc)
def _init_to_identity(self): identity = self.headers.get("as2-to") if is_none_or_whitespace(identity): raise AS2Exception('as2-to is required') self.to_identity = decode_as2_identity(identity.strip())
def _send_data(self, data, context): try: sender = SendEncoder(context.message.message_id, context.message.business_id, data, context) sent_headers, sent_body = sender.encode() target_url = context.agreement.outbound_agreement.target_url ssl_version = context.agreement.outbound_agreement.ssl_version auth = PartnerManager._build_auth(context.agreement) proxies = PartnerManager._build_proxy(context.agreement) customer_http_headers = context.agreement.outbound_agreement.customer_http_headers if customer_http_headers is not None: extend_headers = { item.name: item.value for item in customer_http_headers if item.name is None } sent_headers.update(extend_headers) context.message.sent_headers = [ NameValuePair(name=k, value=v) for k, v in sent_headers.items() ] if context.agreement.profiler_enabled: status, url = self._upload_data_to_dfs( "{key}.out".format(key=context.message.key), sent_body) context.message.sent_data_dfs_path = url context.message.message_is_mic = sender.is_mic context.message.message_mic_digest = sender.mic_digest context.message.message_mic_algorithm = sender.mic_algorithm resp = HttpHelper.post(url=target_url, data=sent_body, headers=sent_headers, proxies=proxies, auth=auth, verify=False) status_code, received_headers, received_body = resp.status_code, dict( resp.headers), resp.content context.message.received_status_code = status_code context.message.received_headers = [ NameValuePair(name=k, value=v) for k, v in received_headers.items() ] if context.agreement.profiler_enabled and not is_none_or_whitespace( received_body): status, url = self._upload_data_to_dfs( "{key}.in".format(key=context.message.key), received_body) context.message.received_data_dfs_path = url if status_code != requests.codes.ok: resp.raise_for_status() context.message.message_status = StatusType.successful return received_headers, received_body except AS2Exception: raise except: logger.exception( "send data failed; business-id: {business_id}, message-id: {message_id}" .format(business_id=context.message.business_id, message_id=context.message.message_id)) raise Exception("send data failed; due to: {message}".format( message=str(sys.exc_info()[1])))
def _set_validate_status(self): if self.disposition_type == 'processed' and is_none_or_whitespace( self.disposition_modifier_code): self.validate_status = StatusType.successful else: self.validate_status = StatusType.failed
def _init_mdn_message_content_type(self): content_type = self.headers.get("content-type") if is_none_or_whitespace(content_type): raise AS2MdnException('mdn-content-type is required') self.mdn_message_content_type = content_type.strip()
def _init_content_type(self): content_type = self.headers.get("content-type") if is_none_or_whitespace(content_type): raise AS2Exception('content-type is required') self.content_type = content_type.lower().strip()