Skip to content

Commit

Permalink
feat: add endpoint in witness to query tel event of an aid (#887)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* cleanup imports

Signed-off-by: arshdeep singh <[email protected]>

---------

Signed-off-by: arshdeep singh <[email protected]>
  • Loading branch information
Arsh-Sandhu authored Jan 8, 2025
1 parent cc02656 commit db1fc37
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 5 deletions.
103 changes: 103 additions & 0 deletions src/keri/app/indirecting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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&reg=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
124 changes: 119 additions & 5 deletions tests/app/test_indirecting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
"""
import json
import time

import falcon
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.app import indirecting, storing, habbing
from keri.core import coring, serdering
from keri import kering
from keri import core
from keri.app import indirecting, storing, habbing, agenting


def test_mailbox_iter():
Expand Down Expand Up @@ -104,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)
Expand Down Expand Up @@ -152,6 +157,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
Expand Down Expand Up @@ -183,3 +296,4 @@ def test_createHttpServer(monkeypatch):
if __name__ == "__main__":
test_mailbox_iter()
test_qrymailbox_iter()
test_wit_query_ends()

0 comments on commit db1fc37

Please sign in to comment.