initial commit

This commit is contained in:
AF 2021-11-27 17:43:43 +03:00
commit 7b884dd4f1
18 changed files with 602 additions and 0 deletions

215
.gitignore vendored Normal file
View File

@ -0,0 +1,215 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
/data/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,51 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="markdown" />
<item index="7" class="java.lang.String" itemvalue="sv3i" />
<item index="8" class="java.lang.String" itemvalue="sv3o" />
<item index="9" class="java.lang.String" itemvalue="sv3a" />
<item index="10" class="java.lang.String" itemvalue="sv3c" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="nacl" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="PySide2.QtWidgets.clicked.connect" />
<option value="PySide2.QtWidgets.valueChanged.connect" />
<option value="PySide2.QtWidgets.textChanged.connect" />
<option value="PySide2.QtCore.Signal.emit" />
<option value="PySide2.QtCore.Signal.connect" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (v6d0auth)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/v6d0auth.iml" filepath="$PROJECT_DIR$/.idea/v6d0auth.iml" />
</modules>
</component>
</project>

10
.idea/v6d0auth.iml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
aiohttp~=3.8.1
PyNaCl~=1.4.0

0
v6d0auth/__init__.py Normal file
View File

87
v6d0auth/app.py Normal file
View File

@ -0,0 +1,87 @@
import asyncio.futures
from nacl.encoding import URLSafeBase64Encoder
from aiohttp import web, http_websocket
from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey
from v6d0auth import certs, cdb
__all__ = ('get_app',)
def define_routes(routes: web.RouteTableDef, loop: asyncio.AbstractEventLoop):
print(certs.vkey.encode(URLSafeBase64Encoder).decode())
mycdb = cdb.CDB(loop)
loop.create_task(mycdb.job())
@routes.get('/')
async def home(_request: web.Request):
return web.Response(body='v6d0auth\n')
@routes.post('/approve')
async def approve(request: web.Request):
try:
cert = mycdb.approve(await request.read())
except BadSignatureError:
raise web.HTTPUnauthorized
except KeyError:
raise web.HTTPNotFound
else:
return web.Response(body=cert)
@routes.post('/push')
async def push(request: web.Request):
try:
timeout = mycdb.push(VerifyKey(await request.read())).timeout
except KeyError:
raise web.HTTPTooManyRequests
else:
return web.Response(body=str(timeout))
@routes.post('/pull')
async def pull(request: web.Request):
try:
cert = mycdb.pull(VerifyKey(await request.read()))
except KeyError:
raise web.HTTPNotFound
else:
return web.Response(body=cert)
@routes.get('/pullws')
async def pullws(request: web.Request):
ws = web.WebSocketResponse()
await ws.prepare(request)
try:
srq = mycdb.requester_mapping[VerifyKey(await ws.receive_bytes())]
except TypeError:
await ws.close(code=http_websocket.WSCloseCode.POLICY_VIOLATION)
else:
try:
cert = await srq.future
except asyncio.CancelledError:
if not srq.future.cancelled():
srq.future.cancel()
if not srq.cancelled:
srq.future = asyncio.get_event_loop().create_future()
await ws.close(code=http_websocket.WSCloseCode.TRY_AGAIN_LATER)
else:
await ws.send_bytes(cert)
await ws.close()
return ws
def app_routes(loop: asyncio.AbstractEventLoop) -> web.RouteTableDef:
routes = web.RouteTableDef()
define_routes(routes, loop)
return routes
def app_with_routes(routes: web.RouteTableDef):
app = web.Application()
app.add_routes(routes)
return app
def get_app(loop: asyncio.AbstractEventLoop) -> web.Application:
return app_with_routes(app_routes(loop))

93
v6d0auth/cdb.py Normal file
View File

@ -0,0 +1,93 @@
import asyncio
import functools
import heapq
import time
import weakref
from typing import MutableMapping, Optional
from nacl.encoding import URLSafeBase64Encoder
from nacl.signing import VerifyKey
from nacl.utils import random
from v6d0auth import certs
__all__ = ('CDB',)
TIMEOUT = 300
@functools.total_ordering
class SignatureRequest:
def __init__(self, requester: VerifyKey, loop: asyncio.AbstractEventLoop):
self._requester = requester
self.timeout = time.time() + TIMEOUT
self.handle: bytes = random(12)
self.approved: Optional[bytes] = None
self.cancelled = False
self.future: asyncio.Future[bytes] = loop.create_future()
def __le__(self, other):
if isinstance(other, SignatureRequest):
return self.timeout < other.timeout
else:
return NotImplemented
def timed_out(self) -> bool:
return time.time() > self.timeout
def approve(self) -> bytes:
if self.approved is None:
self.approved = certs.sign(bytes(self._requester))
self.future.set_result(self.approved)
print('approved', self.handle.hex())
return self.approved
def cancel(self):
if not self.future.done():
self.future.cancel()
self.cancelled = True
class CDB:
def __init__(self, loop: asyncio.AbstractEventLoop):
self.handle_mapping: MutableMapping[bytes, SignatureRequest] = weakref.WeakValueDictionary()
self.requester_mapping: MutableMapping[VerifyKey, SignatureRequest] = weakref.WeakValueDictionary()
self.heap: list[SignatureRequest] = []
self._loop = loop
def _cleanup(self):
while self.heap and self.heap[0].timed_out():
request = heapq.heappop(self.heap)
request.cancel()
yield request
def cleanup(self):
if self.heap:
print('cleaning up')
for request in self._cleanup():
print('cleaned', request.handle.hex())
def push(self, requester: VerifyKey) -> SignatureRequest:
if requester in self.requester_mapping:
raise KeyError
request = SignatureRequest(requester, self._loop)
self.requester_mapping[requester] = request
heapq.heappush(self.heap, request)
self.handle_mapping[request.handle] = request
print('requested', request.handle.hex(), requester.encode(URLSafeBase64Encoder).decode())
return request
def _approve(self, handle: bytes) -> bytes:
return self.handle_mapping[handle].approve()
def approve(self, data: bytes) -> bytes:
handle = certs.verify(data)
return self._approve(handle)
def pull(self, vkey: VerifyKey) -> Optional[bytes]:
return self.requester_mapping[vkey].approved
async def job(self):
while True:
await asyncio.sleep(TIMEOUT)
self.cleanup()

35
v6d0auth/certs.py Normal file
View File

@ -0,0 +1,35 @@
from typing import Optional
from nacl.public import PrivateKey, PublicKey
from nacl.signing import SigningKey, VerifyKey, SignedMessage
from v6d0auth.config import myroot, cakey
__all__ = ('vkey', 'pkey',)
_keyfile = myroot / '.key'
if _keyfile.exists():
_skey = SigningKey(_keyfile.read_bytes())
else:
_skey = SigningKey.generate()
_keyfile.write_bytes(bytes(_skey))
_skey: SigningKey
vkey: VerifyKey = _skey.verify_key
_ekey: PrivateKey = _skey.to_curve25519_private_key()
pkey: PublicKey = _ekey.public_key
def sign(data: bytes) -> SignedMessage:
return _skey.sign(data)
def verify(data: bytes, signature: Optional[bytes] = None) -> bytes:
return vkey.verify(data, signature)
if cakey:
akey: VerifyKey = VerifyKey(cakey)
def averify(data: bytes, signature: Optional[bytes] = None) -> bytes:
return akey.verify(data, signature)

18
v6d0auth/client.py Normal file
View File

@ -0,0 +1,18 @@
import aiohttp
__all__ = ('request_signature',)
from v6d0auth import certs
async def request_signature(base_url: str) -> bytes:
async with aiohttp.ClientSession() as session:
async with session.post(f'{base_url}/push', data=certs.vkey.encode()) as response:
if response.status not in [200, 429]:
raise RuntimeError
async with session.ws_connect(f'{base_url}/pullws') as ws:
await ws.send_bytes(certs.vkey.encode())
try:
return await ws.receive_bytes()
except TypeError:
raise TimeoutError

11
v6d0auth/config.py Normal file
View File

@ -0,0 +1,11 @@
import os
from pathlib import Path
__all__ = ('myroot', 'port', 'cakey')
_root = Path(os.getenv('v6root', './data'))
assert _root.exists()
myroot = _root / 'v6d0auth'
myroot.mkdir(exist_ok=True)
port = int(os.getenv('v6root', '5003'))
cakey = bytes.fromhex(os.getenv('v6ca', ''))

9
v6d0auth/run-server.py Normal file
View File

@ -0,0 +1,9 @@
import asyncio
from aiohttp import web
from v6d0auth.app import get_app
if __name__ == '__main__':
loop = asyncio.get_event_loop()
web.run_app(get_app(loop), host='127.0.0.1', port=5003, loop=loop)

27
v6d0auth/sign-request.py Normal file
View File

@ -0,0 +1,27 @@
import argparse
import asyncio
import aiohttp
from v6d0auth import certs
from v6d0auth.config import port
parser = argparse.ArgumentParser()
parser.add_argument('handle', type=str)
async def main():
handle = bytes.fromhex(args.handle)
request = certs.sign(handle)
async with aiohttp.ClientSession() as session:
async with session.post(f'http://127.0.0.1:{port}/approve', data=request) as response:
print(response.status)
if response.status == 200:
print((await response.read()).hex())
else:
print(await response.text())
if __name__ == '__main__':
args = parser.parse_args()
asyncio.run(main())

12
v6d0auth/test-request.py Normal file
View File

@ -0,0 +1,12 @@
import asyncio
from v6d0auth.client import request_signature
from v6d0auth.config import port
async def main():
print((await request_signature(f'http://127.0.0.1:{port}')).hex())
if __name__ == '__main__':
asyncio.run(main())