import binascii
import struct
import datetime
import secrets
import socket
import random
from time import sleep

class opcua_pack():
    def __init__(self):
        self.SecureChannelId=27
        self.Security_Token_Id = 1
        self.identifier_string = ""
        self.int_chunk_seq = 5
        self.float_chunk_seq = 4
        self.cs=5


    def helf(self,address):
        MSG_HEL = "HEL"
        chunk_type = "F"
        protocol_version = 0
        ReceiveBufferSize = 2147483647
        SendBufferSize = 2147483647
        max_message_size = 0
        max_chunk_count = 0
        endpoint_url = address
        endpoint_url_len = len(endpoint_url)
        message_size=int(8+len(protocol_version.to_bytes(4, byteorder='little').hex()+SendBufferSize.to_bytes(4, byteorder='little').hex()+ReceiveBufferSize.to_bytes(4, byteorder='little').hex()+max_message_size.to_bytes(4, byteorder='little').hex()+max_chunk_count.to_bytes(4, byteorder='little').hex()+endpoint_url_len.to_bytes(4, byteorder='little').hex()+endpoint_url.encode().hex())/2)
        return MSG_HEL.encode()+chunk_type.encode()+message_size.to_bytes(4, byteorder='little')+protocol_version.to_bytes(4, byteorder='little')+SendBufferSize.to_bytes(4, byteorder='little')+ReceiveBufferSize.to_bytes(4, byteorder='little')+max_message_size.to_bytes(4, byteorder='little')+max_chunk_count.to_bytes(4, byteorder='little')+endpoint_url_len.to_bytes(4, byteorder='little')+endpoint_url.encode()

    def opnf(self):
        MSG_OPN = "OPN"
        chunk_type = "F"
        Message_Size = 132
        SecureChannelId = 0
        SecurityPolicyUri = 'http://opcfoundation.org/UA/SecurityPolicy#None'
        SecurityPolicyUrilen=len(SecurityPolicyUri)
        SenderCertificate = binascii.unhexlify('ffffffff')
        ReceiverCertificateThumbprint = binascii.unhexlify('ffffffff')
        SequenceNumber = 1
        RequestId= 1
        NodeId_EncodingMask= 1
        NodeId_Namespace_Index= 0
        NodeId_Identifier_Numeric= 446
        auth_EncodingMask= 0
        Identifier_Numeric= 0

        now = datetime.datetime.utcnow()
        win32_epoch = datetime.datetime(1601, 1, 1)
        interval = int((now - win32_epoch).total_seconds() * 10000000)
        timestamp_bytes = struct.pack('<Q', interval)
        Timestamp= timestamp_bytes
        RequestHandle= 1
        SymbolicId= 0
        LocalizedText= 0
        AdditionalInfo= 0
        Inner_StatusCode= 0
        Inner_Diagnostics= 0
        op_SymbolicId= 0
        op_LocalizedText= 0
        op_AdditionalInfo= 0
        op_Inner_StatusCode= 0
        op_Inner_Diagnostics= 0
        Return_Diagnostics = binascii.unhexlify(hex(int(str('000000')+str(op_Inner_Diagnostics)+str(op_Inner_StatusCode)+ str(op_AdditionalInfo)+str(op_LocalizedText)+str(op_SymbolicId)+str(Inner_Diagnostics)+str(Inner_StatusCode)+str(AdditionalInfo)+str(LocalizedText)+str(SymbolicId),2))[2:].zfill(8))
        AuditEntryId= binascii.unhexlify('ffffffff')
        TimeoutHint= 1000
        ah_EncodingMask= '0000'
        has_server_index= 0
        has_namespace_uri= 0
        additional_header_enc_mask=binascii.unhexlify(hex(int(str(has_namespace_uri)+str(has_server_index)+str('00')+str(ah_EncodingMask),2))[2:].zfill(2))
        ah_Identifier_Numeric=0
        has_binary_body = 0
        has_xml_body = 0
        ah_EncodingMask2 = binascii.unhexlify(hex(int(str('000000')+str(has_xml_body)+str(has_binary_body),2))[2:].zfill(2))
        ClientProtocolVersion=0
        SecurityTokenRequestType=0
        MessageSecurityMode= 1
        ClientNonce= 0
        RequestedLifetime= 3600000
        opnfp=MSG_OPN.encode()+chunk_type.encode()+Message_Size.to_bytes(4, byteorder='little')+SecureChannelId.to_bytes(4, byteorder='little')+SecurityPolicyUrilen.to_bytes(4, byteorder='little')+SecurityPolicyUri.encode()+SenderCertificate+ReceiverCertificateThumbprint+SequenceNumber.to_bytes(4, byteorder='little')+RequestId.to_bytes(4, byteorder='little')+NodeId_EncodingMask.to_bytes(1,byteorder='little')+NodeId_Namespace_Index.to_bytes(1,byteorder='little')+NodeId_Identifier_Numeric.to_bytes(2,byteorder='little')+auth_EncodingMask.to_bytes(1,byteorder='little')+Identifier_Numeric.to_bytes(1,byteorder='little')+Timestamp+RequestHandle.to_bytes(4,byteorder='little')+Return_Diagnostics+AuditEntryId+TimeoutHint.to_bytes(4,byteorder='little')+additional_header_enc_mask+ah_Identifier_Numeric.to_bytes(1,byteorder='little')+ah_EncodingMask2+ClientProtocolVersion.to_bytes(4, byteorder='little')+SecurityTokenRequestType.to_bytes(4, byteorder='little')+MessageSecurityMode.to_bytes(4, byteorder='little')+ClientNonce.to_bytes(4, byteorder='little')+RequestedLifetime.to_bytes(4, byteorder='little')
        return opnfp

    def create_session(self,secure_id,token_id):
        MSG_MSG = "MSG"
        chunk_type = "F"
        Message_Size = 291
        SecureChannelId = secure_id
        Security_Token_Id = token_id
        Security_Sequence_Number = 2
        Security_RequestId = 2
        NodeId_EncodingMask = 1
        NodeId_Namespace_Index = 0
        NodeId_Identifier_Numeric = 461
        auth_EncodingMask = 0
        Identifier_Numeric = 0
        now = datetime.datetime.utcnow()
        win32_epoch = datetime.datetime(1601, 1, 1)
        interval = int((now - win32_epoch).total_seconds() * 10000000)
        timestamp_bytes = struct.pack('<Q', interval)
        Timestamp = timestamp_bytes
        RequestHandle = 2
        SymbolicId = 0
        LocalizedText = 0
        AdditionalInfo = 0
        Inner_StatusCode = 0
        Inner_Diagnostics = 0
        op_SymbolicId = 0
        op_LocalizedText = 0
        op_AdditionalInfo = 0
        op_Inner_StatusCode = 0
        op_Inner_Diagnostics = 0
        Return_Diagnostics = binascii.unhexlify(hex(int(
            str('000000') + str(op_Inner_Diagnostics) + str(op_Inner_StatusCode) + str(op_AdditionalInfo) + str(
                op_LocalizedText) + str(op_SymbolicId) + str(Inner_Diagnostics) + str(Inner_StatusCode) + str(
                AdditionalInfo) + str(LocalizedText) + str(SymbolicId), 2))[2:].zfill(8))
        AuditEntryId = binascii.unhexlify('ffffffff')
        TimeoutHint = 1000
        ah_EncodingMask = '0000'
        has_server_index = 0
        has_namespace_uri = 0
        additional_header_enc_mask = binascii.unhexlify(
            hex(int(str(has_namespace_uri) + str(has_server_index) + str('00') + str(ah_EncodingMask), 2))[2:].zfill(2))
        ah_Identifier_Numeric = 0
        has_binary_body = 0
        has_xml_body = 0
        ah_EncodingMask2 = binascii.unhexlify(
            hex(int(str('000000') + str(has_xml_body) + str(has_binary_body), 2))[2:].zfill(2))
        ApplicationUri="urn:freeopcua:client"
        ApplicationUrilen=len(ApplicationUri.encode()).to_bytes(4,byteorder="little")
        ProductUri = "urn:freeopcua.github.io:client"
        ProductUrilen=len(ProductUri.encode()).to_bytes(4,byteorder="little")
        has_locale_information=0
        has_text=1
        Application_name = binascii.unhexlify(
            hex(int(str('000000') + str(has_text) + str(has_locale_information), 2))[2:].zfill(2))
        Text = "Pure Python Client"
        Textlen = len(Text.encode()).to_bytes(4,byteorder="little")
        ApplicationType = binascii.unhexlify("01000000")
        GatewayServerUri = binascii.unhexlify("ffffffff")
        DiscoveryProfileUri = binascii.unhexlify("ffffffff")
        ArraySize = 0
        ServerUri= binascii.unhexlify("ffffffff")
        EndpointUrl= "opc.tcp://2.2.2.5:53531/myopc/free"
        EndpointUrllen= len(EndpointUrl.encode()).to_bytes(4,byteorder="little")
        SessionName= "Pure Python Client Session1"
        SessionNamelen=len(SessionName.encode()).to_bytes(4,byteorder="little")
        client_nonce = secrets.token_bytes(32)
        client_noncelen = len(client_nonce).to_bytes(4,byteorder="little")
        ClientCertificate = binascii.unhexlify("ffffffff")
        RequestedSessionTimeout= binascii.unhexlify("0000000040774B41")
        MaxResponseMessageSize=  0


        Message_body = SecureChannelId.to_bytes(4, byteorder='little') + \
                    Security_Token_Id.to_bytes(4, byteorder='little') + \
                    Security_Sequence_Number.to_bytes(4, byteorder='little') + \
                    Security_RequestId.to_bytes(4, byteorder='little') + \
                    NodeId_EncodingMask.to_bytes(1, byteorder='little') + \
                    NodeId_Namespace_Index.to_bytes(1, byteorder='little') + \
                    NodeId_Identifier_Numeric.to_bytes(2, byteorder='little') + \
                    auth_EncodingMask.to_bytes(1, byteorder='little') + \
                    Identifier_Numeric.to_bytes(1, byteorder='little') + \
                    Timestamp + \
                    RequestHandle.to_bytes(4, byteorder='little') + \
                    Return_Diagnostics + \
                    AuditEntryId + \
                    TimeoutHint.to_bytes(4, byteorder='little') + \
                    additional_header_enc_mask + \
                    ah_Identifier_Numeric.to_bytes(1, byteorder="little") + \
                    ah_EncodingMask2 + \
                    ApplicationUrilen + \
                    ApplicationUri.encode() + \
                    ProductUrilen + \
                    ProductUri.encode() + \
                    Application_name + \
                    Textlen + \
                    Text.encode() + \
                    ApplicationType + \
                    GatewayServerUri + \
                    DiscoveryProfileUri + \
                    ArraySize.to_bytes(4, byteorder="little") + \
                    ServerUri + \
                    EndpointUrllen + \
                    EndpointUrl.encode() + \
                    SessionNamelen + \
                    SessionName.encode() + \
                    client_noncelen + \
                    client_nonce + \
                    ClientCertificate + \
                    RequestedSessionTimeout + \
                    MaxResponseMessageSize.to_bytes(4, byteorder="little")
        Message_Size=int(8+len(Message_body)).to_bytes(4,byteorder="little")
        session= MSG_MSG.encode()+\
                 chunk_type.encode() +\
                 Message_Size+ \
                 Message_body
        return session


    def start_session(self,secure_id,token_id,identifier_string):
        MSG_MSG = "MSG"
        chunk_type = "F"
        Message_Size = 188
        SecureChannelId = secure_id
        Security_Token_Id = token_id
        Security_Sequence_Number = 3
        Security_RequestId = 3
        NodeId_EncodingMask = 1
        NodeId_Namespace_Index = 0
        NodeId_Identifier_Numeric = 467
        auth_EncodingMask = 5
        Namespace_Index= 0
        identifier_bytestring = binascii.unhexlify(identifier_string)
        identifier_bytestringlen= len(identifier_bytestring).to_bytes(4,byteorder="little")
        now = datetime.datetime.utcnow()
        win32_epoch = datetime.datetime(1601, 1, 1)
        interval = int((now - win32_epoch).total_seconds() * 10000000)
        timestamp_bytes = struct.pack('<Q', interval)
        Timestamp = timestamp_bytes
        Request_handle = 3
        SymbolicId = 0
        LocalizedText = 0
        AdditionalInfo = 0
        Inner_StatusCode = 0
        Inner_Diagnostics = 0
        op_SymbolicId = 0
        op_LocalizedText = 0
        op_AdditionalInfo = 0
        op_Inner_StatusCode = 0
        op_Inner_Diagnostics = 0
        Return_Diagnostics = binascii.unhexlify(hex(int(
            str('000000') + str(op_Inner_Diagnostics) + str(op_Inner_StatusCode) + str(op_AdditionalInfo) + str(
                op_LocalizedText) + str(op_SymbolicId) + str(Inner_Diagnostics) + str(Inner_StatusCode) + str(
                AdditionalInfo) + str(LocalizedText) + str(SymbolicId), 2))[2:].zfill(8))
        AuditEntryId = binascii.unhexlify('ffffffff')
        TimeoutHint = 1000
        ah_EncodingMask="0000"
        has_server_index=0
        has_namespace_uri=0
        additional_header_enc_mask=binascii.unhexlify(
            hex(int( str(has_namespace_uri) + str(has_server_index)+str(ah_EncodingMask), 2))[2:].zfill(2))
        Identifier_Numeric= 0
        has_binary_body = 0
        has_xml_body = 0
        ah_EncodingMask2 = binascii.unhexlify(
            hex(int(str('000000') + str(has_xml_body) + str(has_binary_body), 2))[2:].zfill(2))
        Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"
        Algorithmlen=len(Algorithm.encode()).to_bytes(4,byteorder="little")
        Signature=int(0).to_bytes(4,byteorder="little")
        client_ArraySize = 0
        locale_ArraySize = 1
        LocaleIds = "en"
        LocaleIdslen = len(LocaleIds.encode()).to_bytes(4,byteorder="little")
        uit_encoding_mask='0001'
        has_server_index= 0
        has_namespace_uri=0
        uit_token=binascii.unhexlify(
            hex(int(str(has_namespace_uri)+str(has_server_index)+str('00')+str(uit_encoding_mask), 2))[2:].zfill(2))
        uit_Namespace_Index = 0
        uit_Identifier_Numeric = 321
        uit_has_binary_body = 1
        uit_has_xml_body = 0
        uit_EncodingMask2 = binascii.unhexlify(
            hex(int(str('000000') + str(uit_has_xml_body) + str(uit_has_binary_body), 2))[2:].zfill(2))
        PolicyId= "anonymous"
        PolicyIdlen=len(PolicyId.encode()).to_bytes(4,byteorder="little")
        aitlen=(len(PolicyIdlen)+len(PolicyId.encode())).to_bytes(4,byteorder="little")
        uts_algorithm=binascii.unhexlify("ffffffff")
        uts_signature=binascii.unhexlify("ffffffff")


        session = MSG_MSG.encode() + \
                  chunk_type.encode() + \
                  Message_Size.to_bytes(4, byteorder='little') + \
                  SecureChannelId.to_bytes(4, byteorder='little') + \
                  Security_Token_Id.to_bytes(4, byteorder='little') + \
                  Security_Sequence_Number.to_bytes(4, byteorder='little') + \
                  Security_RequestId.to_bytes(4, byteorder='little') + \
                  NodeId_EncodingMask.to_bytes(1, byteorder='little') + \
                  NodeId_Namespace_Index.to_bytes(1, byteorder='little') + \
                  NodeId_Identifier_Numeric.to_bytes(2, byteorder='little') + \
                  auth_EncodingMask.to_bytes(1, byteorder='little')+ \
                  Namespace_Index.to_bytes(2, byteorder='little')+ \
                  identifier_bytestringlen+\
            identifier_bytestring+\
            Timestamp+\
            Request_handle.to_bytes(4,byteorder="little")+ \
                  Return_Diagnostics+ \
                  AuditEntryId+ \
                  TimeoutHint.to_bytes(4,byteorder="little")+\
                  additional_header_enc_mask+\
        Identifier_Numeric.to_bytes(1,byteorder="little")+\
                  ah_EncodingMask2+\
                  Algorithmlen+\
            Algorithm.encode()+\
            Signature+\
            client_ArraySize.to_bytes(4,byteorder="little")+\
        locale_ArraySize.to_bytes(4,byteorder="little")+\
            LocaleIdslen+\
            LocaleIds.encode()+\
            uit_token+\
            uit_Namespace_Index.to_bytes(1,byteorder="little")+\
            uit_Identifier_Numeric.to_bytes(2,byteorder="little")+\
                  uit_EncodingMask2+\
                  aitlen+\
                  PolicyIdlen+\
            PolicyId.encode()+ \
                  uts_algorithm+ \
                  uts_signature
        return session



    def write_request(self,secure_id,token_id,identifier_string,chunk_seq,ns,nodeid,value,datatype):
        source_timestamp=b''
        MSG_MSG = "MSG"
        chunk_type = "F"
        SecureChannelId = secure_id
        Security_Token_Id = token_id
        Security_Sequence_Number = chunk_seq
        Security_RequestId = chunk_seq
        NodeId_EncodingMask = 1
        NodeId_Namespace_Index = 0
        NodeId_Identifier_Numeric = 673
        auth_EncodingMask = 5
        Namespace_Index = 0
        identifier_bytestring = binascii.unhexlify(identifier_string)
        identifier_bytestringlen = len(identifier_bytestring).to_bytes(4, byteorder="little")
        now = datetime.datetime.utcnow()
        win32_epoch = datetime.datetime(1601, 1, 1)
        interval = int((now - win32_epoch).total_seconds() * 10000000)
        timestamp_bytes = struct.pack('<Q', interval)
        Timestamp = timestamp_bytes
        Request_handle = 4
        SymbolicId = 0
        LocalizedText = 0
        AdditionalInfo = 0
        Inner_StatusCode = 0
        Inner_Diagnostics = 0
        op_SymbolicId = 0
        op_LocalizedText = 0
        op_AdditionalInfo = 0
        op_Inner_StatusCode = 0
        op_Inner_Diagnostics = 0
        Return_Diagnostics = binascii.unhexlify(hex(int(
            str('000000') + str(op_Inner_Diagnostics) + str(op_Inner_StatusCode) + str(op_AdditionalInfo) + str(
                op_LocalizedText) + str(op_SymbolicId) + str(Inner_Diagnostics) + str(Inner_StatusCode) + str(
                AdditionalInfo) + str(LocalizedText) + str(SymbolicId), 2))[2:].zfill(8))
        AuditEntryId = binascii.unhexlify('ffffffff')
        TimeoutHint = 1000
        ah_EncodingMask = "0000"
        has_server_index = 0
        has_namespace_uri = 0
        additional_header_enc_mask = binascii.unhexlify(
            hex(int(str(has_namespace_uri) + str(has_server_index) + str(ah_EncodingMask), 2))[2:].zfill(2))
        Identifier_Numeric = 0
        has_binary_body = 0
        has_xml_body = 0
        ah_EncodingMask2 = binascii.unhexlify(
            hex(int(str('000000') + str(has_xml_body) + str(has_binary_body), 2))[2:].zfill(2))
        ArraySize = 1
        wv_encoding_mask = "0010"
        wv_encoding_mask_value = binascii.unhexlify(
            hex(int(str("0000") + str(wv_encoding_mask) , 2))[2:].zfill(2))
        wv_Namespace_Index = ns
        wv_Identifier_Numeric = nodeid
        AttributeId = 13
        IndexRange = binascii.unhexlify("ffffffff")
        dv_has_value=1
        dv_has_status_code=1
        dv_has_source_timestamp = 1 if datatype=="<class 'float'>" else 0
        dv_has_server_timestamp = 0
        dv_has_source_picoseconds = 0
        dv_has_server_picoseconds = 0
        data_value=binascii.unhexlify(
            hex(int(str("00") + str(dv_has_server_picoseconds)+ str(dv_has_source_picoseconds)+ str(dv_has_server_picoseconds)+ str(dv_has_server_timestamp)+ str(dv_has_source_timestamp)+ str(dv_has_status_code)+ str(dv_has_value), 2))[2:].zfill(2))
        if(datatype=="<class 'float'>"):
            vt_type=11
            vt_value=struct.pack('d', value)
            source_timestamp = timestamp_bytes
            Message_Size = 135
        else:
            Message_Size = 123
            vt_type=6
            vt_value = struct.pack('i', int(value))
        statuscode=binascii.unhexlify("00000000")



        session = MSG_MSG.encode() + \
                  chunk_type.encode() + \
                  Message_Size.to_bytes(4, byteorder='little') + \
                  SecureChannelId.to_bytes(4, byteorder='little') + \
                  Security_Token_Id.to_bytes(4, byteorder='little') + \
                  Security_Sequence_Number.to_bytes(4, byteorder='little') + \
                  Security_RequestId.to_bytes(4, byteorder='little') + \
                  NodeId_EncodingMask.to_bytes(1, byteorder='little') + \
                  NodeId_Namespace_Index.to_bytes(1, byteorder='little') + \
                  NodeId_Identifier_Numeric.to_bytes(2, byteorder='little') + \
                  auth_EncodingMask.to_bytes(1, byteorder='little') + \
                  Namespace_Index.to_bytes(2, byteorder='little') + \
                  identifier_bytestringlen + \
                  identifier_bytestring + \
                  Timestamp + \
                  Request_handle.to_bytes(4, byteorder="little") + \
                  Return_Diagnostics + \
                  AuditEntryId + \
                  TimeoutHint.to_bytes(4, byteorder="little") + \
                  additional_header_enc_mask + \
                  Identifier_Numeric.to_bytes(1, byteorder="little") + \
                  ah_EncodingMask2+ \
                  ArraySize.to_bytes(4,byteorder="little")+ \
                  wv_encoding_mask_value+ \
                  wv_Namespace_Index.to_bytes(2,byteorder="little")+ \
                  wv_Identifier_Numeric.to_bytes(4,byteorder="little")+ \
                  AttributeId.to_bytes(4,byteorder="little")+\
                  IndexRange+ \
                  data_value+ \
                  vt_type.to_bytes(1,byteorder="little")+\
                  vt_value+ \
                  statuscode+ \
                  source_timestamp

        return session

    def connect(self,address):
        host,port=address.split("/")[2].split(":")
        server_address = (socket.gethostbyname(host), int(port))
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(server_address)
        message = self.helf(address)
        sock.sendall(message)
        data = sock.recv(10249)
        sock.sendall(self.opnf())
        data = sock.recv(10249)
        self.SecureChannelId = int(data[-24:-23].hex(), 16)
        self.TokenId = int(data[-20:-19].hex(), 16)
        message=self.create_session(self.SecureChannelId, self.TokenId)
        sock.sendall(message)
        count = 0
        while True:
            try:
                sock.settimeout(2)
                data = sock.recv(5720)
                if not data:
                    break
                if count == 0:
                    self.identifier_string = data.hex()[156:156 + 64]
                    count = count + 1
            except:
                break
        data = self.start_session(self.SecureChannelId, self.TokenId, self.identifier_string)
        sock.sendall(data)
        res = sock.recv(1024)
        sock.sendall(self.write_request(self.SecureChannelId, self.TokenId, self.identifier_string, 4, 3, 1002, 1, "<class 'int'>"))
        return sock

    def write_opcua_value(self,sock,ns,nodeid,val,datatype):
        if(datatype=="<class 'float'>"):
            sock.sendall(self.write_request(self.SecureChannelId, self.TokenId, self.identifier_string, self.cs, int(ns), int(nodeid), float(val), datatype))
            self.cs=self.cs+1
        elif(datatype=="<class 'int'>"):
            sock.sendall(self.write_request(self.SecureChannelId, self.TokenId, self.identifier_string, self.cs, int(ns),int(nodeid),float(val), datatype))
            self.cs = self.cs + 1