From 2b5a1e2ebdb850eed63715e6e825193821045c64 Mon Sep 17 00:00:00 2001 From: Arshdeep Date: Tue, 12 Nov 2024 18:29:45 +0530 Subject: [PATCH 1/2] feat: add endpoint in witness to query tel event of an aid - added /query endpoint in witness agent to query for TEL or KEL events using HTTP GET request Signed-off-by: arshdeep singh --- src/keri/app/indirecting.py | 103 +++++++++++++++++++++++++++++ tests/app/test_indirecting.py | 121 ++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) diff --git a/src/keri/app/indirecting.py b/src/keri/app/indirecting.py index 482d79124..e531a4402 100644 --- a/src/keri/app/indirecting.py +++ b/src/keri/app/indirecting.py @@ -88,6 +88,8 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPo app.add_route("/", httpEnd) receiptEnd = ReceiptEnd(hab=hab, inbound=cues, aids=aids) app.add_route("/receipts", receiptEnd) + queryEnd = QueryEnd(hab=hab) + app.add_route("/query", queryEnd) server = createHttpServer(host, httpPort, app, keypath, certpath, cafilepath) if not server.reopen(): @@ -1183,3 +1185,104 @@ def interceptDo(self, tymth=None, tock=0.0): yield self.tock yield self.tock + + +class QueryEnd: + """ Endpoint class for quering witness for KELs and TELs using HTTP GET + + """ + + def __init__(self, hab): + self.hab = hab + self.reger = viring.Reger(name=hab.name, db=hab.db, temp=False) + + def on_get(self, req, rep): + """ Handles GET requests to query KEL or TEL events of a pre from a witness. + + Parameters: + req (Request) Falcon HTTP request + rep (Response) Falcon HTTP response + + Query Parameters: + typ (string): The type of event data to query for. Accepted values are: + - 'kel': Retrieve KEL events for a specified 'pre'. + - 'tel': Retrieve TEL events based on 'reg' or 'vcid'. + pre (string, optional): For 'kel' queries, the specific 'pre' to query. + sn (int, optional): For "kel" queries. If provided, returns events with seq-num + greater than or equal to `sn`. + reg (string, optional): For 'tel' queries, registry pre. required if `vcid` is not provided. + vcid (string, optional): For 'tel' queries, credential said. required if `reg` is not provided. + + Response: + - 200 OK: Returns event data in "application/json+cesr" format. + - 400 Bad Request: Returned if required query parameters are missing or if an invalid `typ` is specified. + + Example: + - /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W + - /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W&sn=5 + - /query?typ=tel®=EHrbPfpRLU9wpFXTzGY-LIo2FjMiljjEnt238eWHb7yZ&vcid=EO5y0jMXS5XKTYBKjCUPmNKPr1FWcWhtKwB2Go2ozvr0 + + """ + + typ = req.get_param("typ") + + if not typ: + raise falcon.HTTPBadRequest(description="'typ' query param is required") + + if typ == "kel": + pre = req.get_param("pre") + + if not pre: + raise falcon.HTTPBadRequest(description="'pre' query param is required") + + evnts = bytearray() + + sn = req.get_param_as_int("sn") + if sn is not None: ## query for event with seq-num >= sn + preb = pre.encode("utf-8") + dig = self.hab.db.getKeLast(key=dbing.snKey(pre=preb, + sn=sn)) + if dig is None: + raise falcon.HTTPBadRequest(description=f"non-existant event at seq-num {sn}") + + for dig in self.hab.db.getKelIter(pre, sn=sn): + try: + msg = self.hab.db.cloneEvtMsg(pre=pre, fn=0, dig=dig) + except Exception: + continue # skip this event + evnts.extend(msg) + else: + for msg in self.hab.db.clonePreIter(pre=pre): + evnts.extend(msg) + + + rep.set_header('Content-Type', "application/json+cesr") + rep.status = falcon.HTTP_200 + rep.data = bytes(evnts) + + elif typ == "tel": + regk = req.get_param("reg") + vcid = req.get_param("vcid") + + if not regk and not vcid: + raise falcon.HTTPBadRequest(description="Either 'reg' or 'vcid' query param is required for TEL query") + + evnts = bytearray() + if regk is not None: + cloner = self.reger.clonePreIter(pre=regk) + for msg in cloner: + evnts.extend(msg) + + if vcid is not None: + cloner = self.reger.clonePreIter(pre=vcid) + for msg in cloner: + evnts.extend(msg) + + rep.set_header('Content-Type', "application/json+cesr") + rep.status = falcon.HTTP_200 + rep.data = bytes(evnts) + + else: + rep.set_header('Content-Type', "application/json") + rep.text = "unkown query type." + rep.status = falcon.HTTP_400 \ No newline at end of file diff --git a/tests/app/test_indirecting.py b/tests/app/test_indirecting.py index 07377d761..b2379f4b8 100644 --- a/tests/app/test_indirecting.py +++ b/tests/app/test_indirecting.py @@ -4,16 +4,28 @@ """ import json +import time import falcon +from falcon import testing import hio import pytest from hio.core import tcp, http +from hio.base import doing, tyming from hio.help import decking +from keri import kering +from keri import core from keri.app import indirecting, storing, habbing from keri.core import coring, serdering +from keri import core +from keri.core import eventing, parsing, coring + +from keri.app import habbing, delegating, indirecting, agenting, notifying + +from keri.db import dbing + def test_mailbox_iter(): pre = "EA3mbE6upuYnFlx68GmLYCQd7cCcwG_AtHM6dW_GT068" @@ -152,6 +164,114 @@ def test_qrymailbox_iter(): next(mbi) +def test_wit_query_ends(seeder): + with habbing.openHby(name="wes", salt=core.Salter(raw=b'wess-the-witness').qb64) as wesHby, \ + habbing.openHby(name="pal", salt=core.Salter(raw=b'0123456789abcdef').qb64) as palHby: + + wesDoers = indirecting.setupWitness(alias="wes", hby=wesHby, tcpPort=5634, httpPort=5644) + witDoer = agenting.Receiptor(hby=palHby) + + wesHab = wesHby.habByName(name="wes") + seeder.seedWitEnds(palHby.db, witHabs=[wesHab], protocols=[kering.Schemes.http]) + + app = falcon.App() + query_endpoint = indirecting.QueryEnd(wesHab) + app.add_route("/query", query_endpoint) + + wesClient = testing.TestClient(app) + + opts = dict( + wesHab=wesHab, + palHby=palHby, + witDoer=witDoer, + wesClient=wesClient + ) + + doers = wesDoers + [witDoer, doing.doify(wit_querier_test_do, **opts)] + + limit = 1.0 + tock = 0.03125 + doist = doing.Doist(tock=tock, limit=limit, doers=doers) + doist.enter() + + tymer = tyming.Tymer(tymth=doist.tymen(), duration=doist.limit) + + while not tymer.expired: + doist.recur() + time.sleep(doist.tock) + # doist.do(doers=doers) + + assert doist.limit == limit + + doist.exit() + + +def wit_querier_test_do(tymth=None, tock=0.0, **opts): + yield tock # enter context + + wesHab = opts["wesHab"] + palHby = opts["palHby"] + witDoer = opts["witDoer"] + wesClient = opts["wesClient"] + + palHab = palHby.makeHab(name="pal", wits=[wesHab.pre], transferable=True) + + assert palHab.pre == "EEWz3RVIvbGWw4VJC7JEZnGCLPYx4-QgWOwAzGnw-g8y" + + witDoer.msgs.append(dict(pre=palHab.pre)) + while not witDoer.cues: + yield tock + + witDoer.cues.popleft() + msg = next(wesHab.db.clonePreIter(pre=palHab.pre)) + + # Test valid KEL query with 'pre' + res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre}) + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr" + assert bytearray(res.content) == bytearray(msg) + + # Test KEL query without 'pre' + res = wesClient.simulate_get("/query", params={"typ": "kel"}) + assert res.status_code == 400 + assert res.headers['Content-Type'] == "application/json" + assert "'pre' query param is required" in res.text + + # Test KEL query with 'sn' parameter + res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre, "sn": 0}) + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr" + + # Test KEL query with non-existant 'sn' parameter + res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre, "sn": 5}) + assert res.status_code == 400 + assert res.headers['Content-Type'] == "application/json" + assert "non-existant event at seq-num 5" in res.text + + # Test valid TEL query with 'reg' + res = wesClient.simulate_get("/query", params={"typ": "tel", "reg": "mock_reg"}) + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr" + + # Test valid TEL query with 'vcid' + res = wesClient.simulate_get("/query", params={"typ": "tel", "vcid": "mock_vcid"}) + assert res.status_code == 200 + assert res.headers['Content-Type'] == "application/json+cesr" + + # Test TEL query missing both 'reg' and 'vcid' + res = wesClient.simulate_get("/query", params={"typ": "tel"}) + assert res.status_code == 400 + assert res.headers['Content-Type'] == "application/json" + assert "Either 'reg' or 'vcid' query param is required for TEL query" in res.text + + # Test invalid 'typ' parameter + res = wesClient.simulate_get("/query", params={"typ": "invalid"}) + assert res.status_code == 400 + assert res.headers['Content-Type'] == "application/json" + assert "unkown query type" in res.text + + + class MockServerTls: def __init__(self, certify, keypath, certpath, cafilepath, port): pass @@ -183,3 +303,4 @@ def test_createHttpServer(monkeypatch): if __name__ == "__main__": test_mailbox_iter() test_qrymailbox_iter() + test_wit_query_ends() From 98f0d3706355253f8d3fa32aa2b5f94ed19a6b25 Mon Sep 17 00:00:00 2001 From: Arshdeep Date: Tue, 12 Nov 2024 18:38:57 +0530 Subject: [PATCH 2/2] cleanup imports Signed-off-by: arshdeep singh --- tests/app/test_indirecting.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/app/test_indirecting.py b/tests/app/test_indirecting.py index b2379f4b8..43815925c 100644 --- a/tests/app/test_indirecting.py +++ b/tests/app/test_indirecting.py @@ -10,21 +10,14 @@ from falcon import testing import hio import pytest -from hio.core import tcp, http + +from hio.core import http from hio.base import doing, tyming from hio.help import decking from keri import kering from keri import core -from keri.app import indirecting, storing, habbing -from keri.core import coring, serdering - -from keri import core -from keri.core import eventing, parsing, coring - -from keri.app import habbing, delegating, indirecting, agenting, notifying - -from keri.db import dbing +from keri.app import indirecting, storing, habbing, agenting def test_mailbox_iter(): @@ -116,9 +109,9 @@ def test_qrymailbox_iter(): with habbing.openHab(name="test", transferable=True, temp=True, salt=b'0123456789abcdef') as (hby, hab): assert hab.pre == 'EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3' icp = hab.makeOwnInception() - icpSrdr = serdering.SerderKERI(raw=icp) + icpSrdr = core.serdering.SerderKERI(raw=icp) qry = hab.query(pre=hab.pre, src=hab.pre, route="/mbx") - srdr = serdering.SerderKERI(raw=qry) + srdr = core.serdering.SerderKERI(raw=qry) cues = decking.Deck() mbx = storing.Mailboxer(temp=True)