From 975f14187921dbcf5a849facd72df2f4fcdec78d Mon Sep 17 00:00:00 2001 From: manatlan Date: Wed, 4 Oct 2023 20:12:07 +0200 Subject: [PATCH] feat: parano & http_only on .handle/.serve too --- README.md | 18 +++++ example.py | 12 +-- htagweb/appserver.py | 164 +++++++++++++++++++++------------------ htagweb/server/client.py | 6 +- test_server.py | 2 +- 5 files changed, 114 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index d7b5326..216a6ca 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,20 @@ Process live as long as the server live (TODO: a TIMEOUT will be back soon) * 'parano mode' (can aes encrypt all communications between client & server ... to avoid mitm'proxies on ws/http interactions) * auto reconnect websocket +### Instanciate + +Like a classical starlette'app : + +```python +from htagweb import AppServer +from yourcode import YourApp # <-- your htag class + +app=AppServer( YourApp, ... ) +if __name__=="__main__": + app.run() +``` + +You can use the following parameters : #### debug (bool) @@ -55,11 +69,15 @@ non-sense in http_only mode. - When False: (default) interactions between front/ui and back are in clear text (json), readable by a MITM. - When True: interactions will be encrypted (less readable by a MITM, TODO: will try to use public/private keys in future) +this parameter is available on `app.handle(request, obj, ... parano=True|False ...)` too, to override defaults ! + #### http_only (bool) - When False: (default) it will use websocket interactions (between front/ui and back), with auto-reconnect feature. - When True: it will use http interactions (between front/ui and back). But "tag.update" feature will not be available. +this parameter is available on `app.handle(request, obj, ... http_only=True|False ...)` too, to override defaults ! + #### session_factory (htagweb.sessions) You can provide a Session Factory to handle the session in different modes. diff --git a/example.py b/example.py index bb4eabb..290322c 100644 --- a/example.py +++ b/example.py @@ -2,7 +2,7 @@ import json,asyncio,time,os """ -Complex htag's app to test: +Complex htag's app for my tests purpose: - a dynamic object (TagSession), which got a render method (new way) - using tag.state (in session) @@ -78,17 +78,11 @@ def test(o): self <= Tag.button("B",_onclick=test) async def handleJo(req): - return await req.app.handle(req,Jo) + return await req.app.handle(req,Jo,http_only=True,parano=True) -# With Web http runner provided by htag -#------------------------------------------------------ -# from htag.runners import WebHTTP -# WebHTTP( App ).run() - -# With htagweb.WebServer runner provided by htagweb #------------------------------------------------------ from htagweb import SimpleServer,AppServer -app=AppServer( App ,parano=False,http_only=True) +app=AppServer( App ) app.add_route("/jo", handleJo ) if __name__=="__main__": diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 96d043b..652a8be 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -124,7 +124,7 @@ class HRSocket(WebSocketEndpoint): async def _sendback(self,websocket, txt:str) -> bool: try: - if websocket.app.parano: + if self.is_parano: seed = parano_seed( websocket.scope["uid"]) txt = crypto.encrypt(txt.encode(),seed) @@ -152,6 +152,7 @@ async def on_connect(self, websocket): uid=websocket.scope["uid"] event=HrClient(uid,fqn).event_response+"_update" #====================================================== + self.is_parano="parano" in websocket.query_params.keys() await websocket.accept() @@ -162,7 +163,7 @@ async def on_receive(self, websocket, data): fqn=websocket.path_params.get("fqn","") uid=websocket.scope["uid"] - if websocket.app.parano: + if self.is_parano: data = crypto.decrypt(data.encode(),parano_seed( uid )).decode() data=json.loads(data) @@ -208,15 +209,15 @@ async def lifespan(app): class AppServer(Starlette): def __init__(self, obj:"htag.Tag class|fqn|None"=None, + session_factory:"sessions.MemDict|sessions.FileDict|sessions.FilePersistentDict|None"=None, debug:bool=True, ssl:bool=False, parano:bool=False, http_only:bool=False, - session_factory:"sessions.MemDict|sessions.FileDict|sessions.FilePersistentDict|None"=None, ): self.ssl=ssl - self.parano=parano - self.http_only=http_only + self.parano = parano + self.http_only = http_only if session_factory is None: self.sesprovider = sessions.MemDict @@ -242,112 +243,125 @@ def __init__(self, if obj: async def handleHome(request): - return await self.serve(request,obj) + return await self.handle(request,obj,recreate=False,http_only=http_only,parano=parano) self.add_route( '/', handleHome ) # new method - async def handle(self, request, obj:"htag.Tag class|fqn", recreate:bool=False ) -> HTMLResponse: - return await self.serve(request,obj,recreate) + async def handle(self, request, + obj:"htag.Tag class|fqn", + recreate:bool=False, + http_only:"bool|None"=False, + parano:"bool|None"=False ) -> HTMLResponse: + return await self.serve(request,obj,recreate,http_only,parano) # DEPRECATED - async def serve(self, request, obj:"htag.Tag class|fqn", force:bool=False ) -> HTMLResponse: + async def serve(self, request, + obj:"htag.Tag class|fqn", + force:bool=False, + http_only:"bool|None"=False, + parano:"bool|None"=False ) -> HTMLResponse: + + # take default behaviour if not present + is_parano = self.parano if parano is None else parano + is_http_only = self.http_only if http_only is None else http_only + + uid = request.scope["uid"] + args,kargs = commons.url2ak(str(request.url)) fqn=normalize(findfqn(obj)) - if self.parano: + if is_parano: seed = parano_seed( uid ) - jstunnel = crypto.JSCRYPTO - jstunnel += f"\nvar _PARANO_='{seed}'\n" - jstunnel += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" - jstunnel += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" + jslib = crypto.JSCRYPTO + jslib += f"\nvar _PARANO_='{seed}'\n" + jslib += "\nasync function _read_(x) {return await decrypt(x,_PARANO_)}\n" + jslib += "\nasync function _write_(x) {return await encrypt(x,_PARANO_)}\n" + pparano="?parano" else: - jstunnel = "" - jstunnel += "\nasync function _read_(x) {return x}\n" - jstunnel += "\nasync function _write_(x) {return x}\n" + jslib = "" + jslib += "\nasync function _read_(x) {return x}\n" + jslib += "\nasync function _write_(x) {return x}\n" + pparano="" - if self.http_only: + if is_http_only: # interactions use HTTP POST - js = """ -%(jstunnel)s - -async function interact( o ) { - let body = await _write_(JSON.stringify(o)); - let req=await window.fetch("/_/%(fqn)s",{method:"POST", body: body}); - let actions=await req.text(); - action( await _read_(actions) ); -} - -window.addEventListener('DOMContentLoaded', start ); -""" % locals() + js = """%(jslib)s + + async function interact( o ) { + let body = await _write_(JSON.stringify(o)); + let req=await window.fetch("/_/%(fqn)s%(pparano)s",{method:"POST", body: body}); + let actions=await req.text(); + action( await _read_(actions) ); + } + + window.addEventListener('DOMContentLoaded', start ); + """ % locals() else: # interactions use WS protocol = "wss" if self.ssl else "ws" - js = """ - %(jstunnel)s - - async function interact( o ) { - _WS_.send( await _write_(JSON.stringify(o)) ); - } - - // instanciate the WEBSOCKET - let _WS_=null; - let retryms=500; - - function connect() { - _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s"); - _WS_.onopen=function(evt) { - console.log("** WS connected") - document.body.classList.remove("htagoff"); - retryms=500; - start(); - - _WS_.onmessage = async function(e) { - let actions = await _read_(e.data) - action(actions) - }; - - } - - _WS_.onclose = function(evt) { - console.log("** WS disconnected, retry in (ms):",retryms); - document.body.classList.add("htagoff"); - - setTimeout( function() { - connect(); - retryms=retryms*2; - }, retryms); - }; - } - connect(); - - """ % locals() - - p = HrClient(uid,fqn,js,self.sesprovider.__name__,force=force) - - args,kargs = commons.url2ak(str(request.url)) + js = """%(jslib)s + + async function interact( o ) { + _WS_.send( await _write_(JSON.stringify(o)) ); + } + + // instanciate the WEBSOCKET + let _WS_=null; + let retryms=500; + + function connect() { + _WS_= new WebSocket("%(protocol)s://"+location.host+"/_/%(fqn)s%(pparano)s"); + _WS_.onopen=function(evt) { + console.log("** WS connected") + document.body.classList.remove("htagoff"); + retryms=500; + start(); + + _WS_.onmessage = async function(e) { + let actions = await _read_(e.data) + action(actions) + }; + + } + + _WS_.onclose = function(evt) { + console.log("** WS disconnected, retry in (ms):",retryms); + document.body.classList.add("htagoff"); + + setTimeout( function() { + connect(); + retryms=retryms*2; + }, retryms); + }; + } + connect(); + """ % locals() + + p = HrClient(uid,fqn,js,self.sesprovider.__name__,recreate=force) html=await p.start(*args,**kargs) return HTMLResponse(html) async def HRHttp(self,request) -> PlainTextResponse: uid = request.scope["uid"] fqn = request.path_params.get("fqn","") + is_parano="parano" in request.query_params.keys() seed = parano_seed( uid ) p=HrClient(uid,fqn) data = await request.body() - if self.parano: + if is_parano: data = crypto.decrypt(data,seed).decode() data=json.loads(data) actions=await p.interact( oid=data["id"], method_name=data["method"], args=data["args"], kargs=data["kargs"], event=data.get("event") ) txt=json.dumps(actions) - if self.parano: + if is_parano: txt = crypto.encrypt(txt.encode(),seed) return PlainTextResponse(txt) diff --git a/htagweb/server/client.py b/htagweb/server/client.py index bec73df..67e7fd1 100644 --- a/htagweb/server/client.py +++ b/htagweb/server/client.py @@ -15,14 +15,14 @@ TIMEOUT=20 # sec to wait answer from redys server #TODO: set better class HrClient: - def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None,force=False): + def __init__(self,uid:str,fqn:str,js:str=None,sesprovidername=None,recreate=False): """ !!!!!!!!!!!!!!!!!!!! if js|sesprovidername is None : can't do a start() !!!!!!!!!!!!!!!!!!!!!!""" self.uid=uid self.fqn=fqn self.js=js self.bus = redys.v2.AClient() self.sesprovidername=sesprovidername - self.force=force + self.recreate=recreate self.hid=f"{uid}_{fqn}" self.event_response = f"response_{self.hid}" @@ -57,7 +57,7 @@ async def start(self,*a,**k) -> str: js=self.js, init= (a,k), sesprovidername=self.sesprovidername, - force=self.force, + force=self.recreate, )) # wait 1st rendering diff --git a/test_server.py b/test_server.py index e8fe111..71b5956 100644 --- a/test_server.py +++ b/test_server.py @@ -34,7 +34,7 @@ async def test_base( server ): html=await p.start() assert html.startswith("") - p=HrClient(uid,"test_hr:App","//",force=True) + p=HrClient(uid,"test_hr:App","//",recreate=True) html=await p.start() assert html.startswith("")