class A2MXPath(): def __init__(self, A=None, B=None, T=None, UA=None, UB=None, M=None, PB=None, PF=None, PD=None, P=None, SA=None, SB=None, D=None, DS=None): # A = node A public key data (address, sign and encrypt compressed public keys) # B = node B public key data # T = timestamp # UA = AX URI node A # UB = AX URI node B # M = MaxSize # PB = POW broadcast # PF = POW forward # PD = POW direct # P = path proof of work # SA = node A signature (over A, B, T and UA if present) # SB = node B signature (over A, B, T and UB if present) # D = deleted timestamp # DS = deleted signature (over A, B, T, SA, SB and D) # A must always be the smaller binary value if not isinstance(A, ECC): self.__a = ECC(pubkey_data=A) else: self.__a = A if not SA and self.__a.hasPrivkey() and not PB < 0: UA = config['publish_axuri'] if not isinstance(B, ECC): self.__b = ECC(pubkey_data=B) else: self.__b = B if not SB and self.__b.hasPrivkey() and not PB < 0: UB = config['publish_axuri'] def testURI(uri): if uri == None: return if len(uri) > 32: raise ValueError('URI too long') try: host, port = uri.split(':') except ValueError: raise ValueError('Invalid URI') try: int(port) except ValueError: raise ValueError('Invalid URI') validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789' if not all(c in validChars for c in host): raise ValueError('Invalid chars in URI') testURI(UA) testURI(UB) self.__ua = UA self.__ub = UB if self.__a.pubkeyData() > self.__b.pubkeyData(): self.__a, self.__b = self.__b, self.__a self.__ua, self.__ub = self.__ub, self.__ua if T: if T > datetime.datetime.now(datetime.timezone.utc): raise ValueError('timestamp is in the future') self.__t = T else: self.__t = now() assert isinstance(M, int) assert isinstance(PB, float) assert isinstance(PF, float) assert isinstance(PD, float) self.__maxsize = M self.__pb = PB self.__pf = PF self.__pd = PD self.__sigod = OrderedDict() self.__sigod['A'] = self.__a.pubkeyData() self.__sigod['B'] = self.__b.pubkeyData() self.__sigod['T'] = self.__t self.__sigod['M'] = self.__maxsize self.__sigod['PB'] = PB self.__sigod['PF'] = PF self.__sigod['PD'] = PD if self.__ua: self.__sigod['UA'] = self.__ua sigdata_a = BSON.encode(self.__sigod) del self.__sigod['UA'] else: sigdata_a = BSON.encode(self.__sigod) if self.__ub: self.__sigod['UB'] = self.__ub sigdata_b = BSON.encode(self.__sigod) del self.__sigod['UB'] else: sigdata_b = BSON.encode(self.__sigod) self.__sa = SA if SA == None: if self.__a.hasPrivkey(): self.__sa = self.__a.signAddress(sigdata_a) else: verify = self.__a.verifyAddress(SA, sigdata_a) if not verify: raise InvalidDataException('SA signature verify failed.') self.__sb = SB if SB == None: if self.__b.hasPrivkey(): self.__sb = self.__b.signAddress(sigdata_b) else: verify = self.__b.verifyAddress(SB, sigdata_b) if not verify: raise InvalidDataException('SA signature verify failed.') if not (self.__sa or self.__sb): raise ValueError('Invalid signatures.') pow_data = BSON.encode(self.__sigod) if P == True: self.pow_done = None def setpow(nonce): self.__pow = nonce if self.pow_done: self.pow_done() calculatePOW(message=pow_data, difficulty=3.0, callback=setpow) else: self.__pow = P if isinstance(P, int): if not checkPOW(message=pow_data, difficulty=3.0, nonce=P): raise ValueError('Invalid POW') elif P != None: raise ValueError('Invalid POW value') self.__d = D self.__ds = DS if self.__d: if self.__d > datetime.datetime.now(datetime.timezone.utc): raise ValueError('Deleted timestamp is in the future.') if self.__d < self.__t: raise ValueError('Deleted timestamp is older than timestamp.') if not self.isComplete: raise ValueError('Deleted path may not be incomplete.') self.__sigod['SA'] = self.__sa self.__sigod['SB'] = self.__sb self.__sigod['D'] = self.__d sigdata = BSON.encode(self.__sigod) verify = self.__a.verifyAddress(self.__ds, sigdata) or self.__b.verifyAddress(self.__ds, sigdata) if not verify: raise InvalidDataException('DS signature verify failed.') self.__hash = hash(self.AHash + self.BHash) self.__longhash = hashlib.sha256(BSON.encode(self.__sigod)).digest() def __getstate__(self): state = { 'A': self.__a.pubkeyData(), 'B': self.__b.pubkeyData(), 'T': self.__t, 'SA': self.__sa, 'SB': self.__sb, 'M': self.__maxsize, 'PB': self.__pb, 'PF': self.__pf, 'PD': self.__pd } if self.__ua: state['UA'] = self.__ua if self.__ub: state['UB'] = self.__ub if self.__pow: state['P'] = self.__pow if self.__d: state['D'] = self.__d state['DS'] = self.__ds return state def __setstate__(self, state): return A2MXPath.__init__(self, **state) def __hash__(self): return self.__hash @property def longHash(self): return self.__longhash @property def isComplete(self): return self.__sa != None and self.__sb != None and self.__pow != None @property def data(self): return self.__getstate__() @property def A(self): return self.__a.pubkeyData() @property def AHash(self): return self.__a.pubkeyHash() @property def AURI(self): return self.__ua @property def B(self): return self.__b.pubkeyData() @property def BHash(self): return self.__b.pubkeyHash() @property def BURI(self): return self.__ub @property def deleted(self): return self.__d @property def timestamp(self): return self.__t @property def newest_timestamp(self): return self.__d or self.__t def markdelete(self): assert self.__d == None assert 'SA' not in self.__sigod assert 'SB' not in self.__sigod assert 'D' not in self.__sigod self.__sigod['SA'] = self.__sa self.__sigod['SB'] = self.__sb self.__d = now() self.__sigod['D'] = self.__d sigdata = BSON.encode(self.__sigod) if self.__a.hasPrivkey(): self.__ds = self.__a.signAddress(sigdata) elif self.__b.hasPrivkey(): self.__ds = self.__b.signAddress(sigdata) else: raise ValueError('Cannot mark path as deleted without private key.') def __eq__(self, other): if not isinstance(other, A2MXPath): return False return self.A == other.A and self.B == other.B def equal(self, other): if not isinstance(other, A2MXPath): return False return self.data == other.data def otherHash(self, otherHash): if otherHash == self.AHash: return self.BHash elif otherHash == self.BHash: return self.AHash raise ValueError('otherHash is neither A or B.') def ecc(self, h): if h == self.AHash: return self.A if h == self.BHash: return self.B raise ValueError("Hash is neither A nor B") @property def hashes(self): return (self.AHash, self.BHash) def is_better_than(self, other): if self != other: raise ValueError('Cannot compare paths with different nodes') return self.newest_timestamp > other.newest_timestamp def __str__(self): return 'A: {}{} B: {}{} Timestamp: {} M: {} PB: {} PF: {} PD: {} POW: {} Deleted: {}{}'.format( self.__a.pubkeyHashBase58(), " ({})".format(self.__ua) if self.__ua else "", self.__b.pubkeyHashBase58(), " ({})".format(self.__ub) if self.__ub else "", self.__t.isoformat(), self.__maxsize, self.__pb, self.__pf, self.__pd, self.__pow, self.__d.isoformat() if self.__d else False, "" if self.isComplete else " Incomplete")
class A2MXAccess(): def __init__(self, node, sendfun): self.node = node self.sendfun = sendfun self.ecc = None self.auth = False def disconnected(self): if self.auth: connected_clients.remove(self) print("A2MXAccess disconnect", self.ecc.pubkeyHashBase58() if self.ecc else "unknown", "authenticated" if self.auth == True else "not authenticated") def process(self, data): rid = data[:4] bs = BSON.decode(bytes(data[4:]), tz_aware=True) try: value = self.process_bson(bs) except Exception as e: import traceback traceback.print_exc() value = None error = 'Exception occured: {} {}'.format(str(type(e)), str(e)) else: error = None response = {} if isinstance(value, (dict, OrderedDict)): response = value else: if value != None: response = { 'data': value } if error != None: response = { 'error': error } if len(response): bv = BSON.encode(response) self.sendfun(rid + bv) def process_bson(self, bs): if self.ecc == None: self.ecc = ECC(pubkey_data=bs['access']) if self.ecc.pubkeyHash() != self.node.ecc.pubkeyHash(): if mongoclient == None or self.ecc.pubkeyHashBase58() not in mongoclient.database_names(): return { 'error': 'Unknown Node {}'.format(self.ecc.pubkeyHashBase58()) } self.db = mongoclient[self.ecc.pubkeyHashBase58()] print("access request to", self.ecc.pubkeyHashBase58()) else: print("access to me") self.auth = now() return { 'auth': self.auth, 'pubkey': self.node.ecc.pubkeyAddress() } if isinstance(self.auth, datetime.datetime): sigdata = BSON.encode({ 'auth': self.auth }) verify = self.ecc.verifyAddress(bs['sig'], sigdata) lsig = self.node.ecc.signAddress(sigdata) if not verify: return { 'error': 'Not authenticated' } self.auth = True connected_clients.add(self) return { 'sig': lsig } if self.auth != True: return { 'error': 'Not authenticated' } if len(bs) != 1: raise A2MXAccessException('Only one command at a time supported') for k, v in bs.items(): f = getattr(self, k, None) if getattr(f, 'A2MXAccessRequest__marker__', False) != True: raise A2MXAccessException('Invalid request {}'.format(k)) if isinstance(v, (dict, OrderedDict)): return f(**v) elif v == None: return f() raise A2MXAccessException('Invalid argument '.format(v)) @A2MXAccessRequest def getpath(self): p = A2MXPath(self.node.ecc, self.ecc, no_URI=True) return p.data @A2MXAccessRequest def setpath(self, **kwargs): p = A2MXPath(**kwargs) assert p.isComplete self.db['path'].remove() self.db['path'].insert(p.data) self.node.new_path(p) return True @A2MXAccessRequest def paths(self): paths = [ p.data for p in self.node.paths ] return paths @A2MXAccessRequest def find(self, query, rep): return [ x for x in self.db['inbox'].find(query, rep) ] @A2MXAccessRequest def save(self, doc): return self.db['inbox'].save(doc) @A2MXAccessRequest def find_routes(self, src, dst, min_hops, max_hops, max_count): if not src or src == b'\x00': src = self.node.ecc.pubkeyHash() routes = self.node.find_routes_from(src, dst, max_hops) send = [] for route in routes: if len(route) < min_hops: continue send.append(route.routes) if len(send) >= max_count: break return send @A2MXAccessRequest def sendto(self, node, data): return self.node.sendto(node, data)
class A2MXNode(): def __init__(self, selectloop): self.selectloop = selectloop self.pathlist = a2mxpath.PathList() self.streams = [] self.ecc = ECC(keyfile=config['keyfile']) self.connected_nodes = { self.ecc.pubkeyHash(): self } mypub = self.ecc.pubkeyHashBase58() if sys.stdout.isatty(): cwd = os.getcwd().rsplit('/', 1)[1] sys.stdout.write("\x1b]2;{}: {}\x07".format(cwd, mypub)) print("I am", mypub) self.selectloop.tadd(90, self.find_new_peers) def add(self, selectable): self.selectloop.add(selectable) def remove(self, selectable): self.selectloop.remove(selectable) def wadd(self, selectable): self.selectloop.wadd(selectable) def wremove(self, selectable): self.selectloop.wremove(selectable) def add_stream(self, stream): # if stream.remote_ecc.pubkeyHash() in self.connected_nodes: # return False assert stream not in self.streams self.streams.append(stream) self.connected_nodes[stream.remote_ecc.pubkeyHash()] = stream return True def del_stream(self, stream): if stream not in self.streams: return self.streams.remove(stream) assert stream.remote_ecc.pubkeyHash() in self.connected_nodes and self.connected_nodes[stream.remote_ecc.pubkeyHash()] == stream del self.connected_nodes[stream.remote_ecc.pubkeyHash()] if stream.path and stream.path.isComplete: self.del_path(stream.path) def new_path(self, path, stream=None): fromhash = stream.remote_ecc.pubkeyHashBase58() if stream else 'myself' if self.pathlist.new(path, fromhash): self.send_path(path, stream) print(self.pathlist) def send_path(self, path, stream=None): for ostream in self.streams: if ostream == stream: continue if hasattr(ostream, 'forward'): ostream.forward.sendCall('path', path.data) def del_path(self, path): self.pathlist.delete(path) self.send_path(path) def update_nodes(self, path): def up(h): if h not in self.nodes: self.nodes[h] = [] nl = self.nodes[h] if path in nl: nl.remove(path) nl.append(path) up(path.AHash) up(path.BHash) def sendto(self, node, data): if node == self.ecc.pubkeyHash(): data = self.ecc.decrypt(bytes(data)) self.request.parseRequest(data) return # if node not in self.connected_nodes: # try: # a2mxaccess.A2MXAccessStore(node, data) # print("stored data for {}".format(ECC.b58(node))) # except a2mxaccess.A2MXAccessException: # print("cannot send to node {}".format(ECC.b58(node))) # return False self.connected_nodes[node].raw_send(data) return True def find_new_peers(self): self.selectloop.tadd(random.randint(30, 90), self.find_new_peers) if len(self.streams) >= config['connections']: return try: new_hash = random.sample(self.pathlist.axuris.keys() - self.connected_nodes.keys(), 1)[0] except ValueError: return A2MXStream(self, uri='ax://' + self.pathlist.axuris[new_hash], pubkey_hash=new_hash) def find_routes_from(self, src, dst, maxhops=None): if dst not in self.nodes: return [] pathlist = self.nodes[dst] if len(pathlist) == 0: return [] dst_pubc = pathlist[0].pubkeyCompressed(dst) def find_path(pathlist, lasthop, thispath=None, step=1): if maxhops != None and step >= maxhops: return if thispath == None: thispath = [dst_pubc] for path in pathlist: if path.deleted: continue nexthop = path.otherHash(lasthop) nexthop_pubc = path.pubkeyCompressed(nexthop) if nexthop == src: ytp = thispath[:] ytp.append(nexthop_pubc) yield A2MXRoute(ytp) continue if nexthop == dst: continue if nexthop_pubc in thispath: continue tp = thispath[:] tp.append(nexthop_pubc) for p in find_path(self.nodes[lasthop], lasthop, tp, step+1): yield p return find_path(pathlist, dst) def shortest_route(self, src, dst): try: routes = [ x for x in self.find_routes_from(src, dst) ] except KeyError: return A2MXRoute([]) if len(routes) == 0: return A2MXRoute([]) return min(routes, key=len)