Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from timofurrer/master
Various improvements / cleanups
- Loading branch information
Showing
11 changed files
with
410 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,13 @@ | ||
__pycache__ | ||
# vim swap files | ||
*.swp | ||
|
||
# python bytecode files | ||
__pycache__/ | ||
|
||
# python virtualenv | ||
env/ | ||
|
||
# python setuptools | ||
*.egg-info/ | ||
build/ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
language: python | ||
python: | ||
- '3.5' | ||
|
||
install: | ||
- python setup.py build sdist | ||
- pip install -r requirements.txt | ||
|
||
script: nosetests --verbosity 2 | ||
|
||
deploy: | ||
provider: pypi | ||
user: tuxtimo | ||
password: | ||
secure: eNrDuSAQuR0xVFb+ZfHZ93mZBEKGWNnidSHTM+8DkuA3OKGTD/Gf3TGAVLaqqURtChukopOANvoBqTWSHMXe6mDSiTn6cPpqxa7WBcOkF4yzzSVQ+ch/wjgpabzgi+XfT6tTo7KI8JYwgxg4NivN/Iv6uxzVqmc7hxwrSJbwruav1ddvonGb+I4M1LYcvUWTFEwlCvBxr+a9CdQ0+JHTtgdCF0T/71NwQJKmA7NtDaS6hex7kJY/CADBsowECURraB4jsbbqnw8sKUDsi6M5/aHSrC0KlpPrSxoESy4EFB7x77WVQxLTcTOqnQQ1ZazNc0g9YfZ7TqcH6cXjcAxqmx/rsmE9EDpauml4IySJWYvEyCMkS2LsqWZI4Em8t2MRO96Zoou3FPgabuUmwEpLlBIfLxTW+XDt2M3fa7wtHP4rIA3e6ruSmVitdC6gFG0nMdGIC/o6s/99Qqx/05eNm1BQ5PdFCzKyVg82mZsqyoSV/37qaVic3pDYIYmGpsm1G/pS4UdH9qGcjP8P0n/986EBAenOeExg9a53XoDms6C/mkeMqJHkJ6WrEKDmaFiUHWz2D3mFTdpL+xWnpOP/b/4/2XSQVh86z+5wW0iXb+wxMdmMSv9T8VBb5kDWj6/6D/y0V90k1i8d2pkmJ3sJAqWNBfw2pC5D03J7qH8zPdU= | ||
on: | ||
tags: true | ||
distributions: sdist bdist_wheel | ||
repo: neolynx/cirrina |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include cirrina/static/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,200 +1,15 @@ | ||
import asyncio | ||
from aiohttp_jrpc import Service, JError, JResponse | ||
from aiohttp import web, WSMsgType | ||
from cryptography import fernet | ||
from aiohttp_session import setup, get_session, session_middleware | ||
from aiohttp_session.cookie_storage import EncryptedCookieStorage | ||
import base64 | ||
import json | ||
""" | ||
`cirrina` - Opinionated web framework | ||
from aiohttp_jrpc import decode | ||
from functools import wraps | ||
from validictory import validate, ValidationError, SchemaError | ||
Package file | ||
def rpc_valid(schema=None): | ||
""" Validation data by specific validictory configuration """ | ||
def dec(fun): | ||
@wraps(fun) | ||
def d_func(self, ctx, session, data, *a, **kw): | ||
try: | ||
validate(data['params'], schema) | ||
except ValidationError as err: | ||
raise InvalidParams(err) | ||
except SchemaError as err: | ||
raise InternalError(err) | ||
return fun(self, ctx, session, data['params'], *a, **kw) | ||
return d_func | ||
return dec | ||
:license: LGPL, see LICENSE for details | ||
""" | ||
|
||
class Server(): | ||
|
||
login_html = '''<!DOCTYPE HTML> | ||
<html> | ||
<body> | ||
<form method="post" action="/login"> | ||
User name:<br/> | ||
<input type="text" name="username"><br/> | ||
User password:<br/> | ||
<input type="password" name="password"><br/> | ||
<input type="hidden" name="path" value="%s"> | ||
<input type="submit" value="Login"><br/> | ||
</form> | ||
</body> | ||
</html> | ||
''' | ||
|
||
def __init__(self, bind, port): | ||
self.bind = bind | ||
self.port = port | ||
self.loop = asyncio.get_event_loop() | ||
self.app = web.Application(loop=self.loop) #, middlewares=[session_middleware]) | ||
|
||
fernet_key = fernet.Fernet.generate_key() | ||
secret_key = base64.urlsafe_b64decode(fernet_key) | ||
setup(self.app, EncryptedCookieStorage(secret_key)) | ||
self.GET ("/login", self._login) | ||
self.POST("/login", self._auth) | ||
self.login_html = Server.login_html | ||
self.authenticate = self.dummy_auth | ||
self.websockets = [] | ||
|
||
# decorator | ||
def authenticated(func): | ||
async def func_wrapper(self, request): | ||
session = await get_session(request) | ||
if session.new: | ||
response = web.Response(status=302) | ||
response.headers['Location'] = '/login?path='+request.path_qs | ||
return response | ||
return await func(self, request, session) | ||
return func_wrapper | ||
|
||
async def _start(self): | ||
self.srv = await self.loop.create_server(self.app.make_handler(), self.bind, self.port) | ||
|
||
async def _login(self, request): | ||
resp = web.Response(text=(self.login_html%(request.GET.get('path', "/"))), content_type="text/html") | ||
return resp | ||
|
||
async def dummy_auth(self, username, password): | ||
if username == 'test' and password == 'test': | ||
return True | ||
return False | ||
|
||
async def _auth(self, request): | ||
session = await get_session(request) | ||
await request.post() | ||
username = request.POST.get('username') | ||
password = request.POST.get('password') | ||
if username and password: | ||
if await self.authenticate(username, password): | ||
print("User authenticated:", username) | ||
session['username'] = username | ||
response = web.Response(status=302) | ||
response.headers['Location'] = request.POST.get('path', "/") | ||
return response | ||
|
||
print("User authentication failed:", 'username') | ||
response = web.Response(status=302) | ||
response.headers['Location'] = '/login' | ||
session.invalidate() | ||
return response | ||
|
||
async def _ws_handler(self, request): | ||
ws = web.WebSocketResponse() | ||
await ws.prepare(request) | ||
|
||
session = await get_session(request) | ||
if session.new: | ||
print("websocket: not logged in") | ||
ws.send_str(json.dumps({'status': 401, 'text': "Unauthorized"})) | ||
ws.close() | ||
return ws | ||
|
||
self.websockets.append(ws) | ||
|
||
self.websocket_connected(ws, session) | ||
|
||
async for msg in ws: | ||
print("websocket got:", msg) | ||
if msg.type == WSMsgType.TEXT: | ||
self.websocket_message(ws, session, msg.data) | ||
elif msg.type == WSMsgType.ERROR: | ||
print('websocket closed with exception %s' % | ||
ws.exception()) | ||
|
||
self.websockets.remove(ws) | ||
self.websocket_closed(session) | ||
|
||
return ws | ||
|
||
def websocket_broadcast(self, msg): | ||
for ws in self.websockets: | ||
ws.send_str(msg) | ||
|
||
def _rpc_handler(self): | ||
class MyRPC(object): | ||
cirrina = self | ||
|
||
def __new__(cls, ctx): | ||
""" Return on call class """ | ||
return cls.__run(cls, ctx) | ||
|
||
@asyncio.coroutine | ||
def __run(self, ctx): | ||
""" Run service """ | ||
try: | ||
data = yield from decode(ctx) | ||
except ParseError: | ||
return JError().parse() | ||
except InvalidRequest: | ||
return JError().request() | ||
except InternalError: | ||
return JError().internal() | ||
|
||
try: | ||
i_app = getattr(MyRPC.cirrina, data['method']) | ||
i_app = asyncio.coroutine(i_app) | ||
except Exception: | ||
return JError(data).method() | ||
|
||
session = yield from get_session(ctx) | ||
try: | ||
resp = yield from i_app(ctx, session, data) | ||
except InvalidParams: | ||
return JError(data).params() | ||
except InternalError: | ||
return JError(data).internal() | ||
|
||
return JResponse(jsonrpc={ | ||
"id": data['id'], "result": resp | ||
}) | ||
|
||
return MyRPC | ||
|
||
def GET(self, location, handler): | ||
self.app.router.add_route('GET', location, handler) | ||
|
||
def POST(self, location, handler): | ||
self.app.router.add_route('POST', location, handler) | ||
|
||
def WS(self): | ||
self.app.router.add_route('GET', "/ws", self._ws_handler) | ||
|
||
def RPC(self, location): | ||
self.app.router.add_route('POST', location, self._rpc_handler()) | ||
|
||
def STATIC(self, location, path): | ||
self.app.router.add_static(location, path) | ||
|
||
def run(self): | ||
self.loop.run_until_complete(self._start()) | ||
print("Server started at http://%s:%d"%(self.bind, self.port)) | ||
try: | ||
self.loop.run_forever() | ||
except KeyboardInterrupt: | ||
pass | ||
self.loop.close() | ||
print("done") | ||
from .server import Server, rpc_valid | ||
|
||
# expose server class | ||
__all__ = ['Server', 'rpc_valid'] | ||
|
||
# define package metadata | ||
__VERSION__ = '0.1.0' |
Oops, something went wrong.