added v25
This commit is contained in:
parent
f3637d2081
commit
94eb71488d
5
.gitignore
vendored
5
.gitignore
vendored
@ -202,3 +202,8 @@ fabric.properties
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
|
||||
|
||||
# Others
|
||||
*.db
|
||||
/test*.py
|
||||
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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/
|
19
.idea/dataSources.xml
Normal file
19
.idea/dataSources.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="test" uuid="c31e2821-78f4-457d-a96f-a4f0fc76708c">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:D:\source\PyCharm\v25\test.db</jdbc-url>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/license.txt</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/sqlite-jdbc-3.31.1.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
19
.idea/inspectionProfiles/Project_Default.xml
Normal file
19
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<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="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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (v25)" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal 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/v25.iml" filepath="$PROJECT_DIR$/.idea/v25.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
10
.idea/v25.iml
Normal file
10
.idea/v25.iml
Normal 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
6
.idea/vcs.xml
Normal 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>
|
0
v25/__init__.py
Normal file
0
v25/__init__.py
Normal file
0
v25/messaging/__init__.py
Normal file
0
v25/messaging/__init__.py
Normal file
122
v25/messaging/message.py
Normal file
122
v25/messaging/message.py
Normal file
@ -0,0 +1,122 @@
|
||||
import json
|
||||
from time import time
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import nacl.encoding
|
||||
import nacl.public
|
||||
import nacl.secret
|
||||
import nacl.signing
|
||||
import nacl.utils
|
||||
|
||||
from v25.messaging.encoding import Encoding
|
||||
from v25.messaging.subject import Subject, PrivateSubject
|
||||
|
||||
KEY_SIZE = 80
|
||||
SFROM = 0
|
||||
STO = 1
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, sfrom: Subject, sto: Subject, idnonce: bytes, timestamp: Optional[float],
|
||||
editnonce: Optional[bytes], pcontent: Optional[bytes], econtent: Optional[bytes],
|
||||
flags: str):
|
||||
self.sfrom = sfrom
|
||||
self.sto = sto
|
||||
self.idnonce = idnonce
|
||||
self.timestamp = timestamp
|
||||
self.editnonce = editnonce
|
||||
self.pcontent = pcontent
|
||||
self.econtent = econtent
|
||||
self.flags = flags
|
||||
|
||||
@classmethod
|
||||
def send(cls, sfrom: PrivateSubject, sto: Subject, pcontent: bytes):
|
||||
return cls(sfrom, sto, Encoding.nonce(), time(),
|
||||
Encoding.nonce(), pcontent, None,
|
||||
'<unread><unedited>').sealed()
|
||||
|
||||
@classmethod
|
||||
def loads(cls, s: str):
|
||||
d = json.loads(s)
|
||||
return cls(
|
||||
Subject.loads(d['sf']), Subject.loads(d['st']), Encoding.decode(d['in']), d['ts'],
|
||||
Encoding.decode(d['en']), None, Encoding.decode(d['ec']),
|
||||
d['fl']
|
||||
)
|
||||
|
||||
@property
|
||||
def pair(self):
|
||||
return self.sfrom, self.sto
|
||||
|
||||
def seal(self) -> bytes:
|
||||
assert isinstance(self.sfrom, PrivateSubject)
|
||||
scontent: nacl.signing.SignedMessage = self.sfrom.skey.sign(self.pcontent)
|
||||
self.encrypt(scontent)
|
||||
return self.econtent
|
||||
|
||||
def encrypt(self, scontent: bytes):
|
||||
key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
|
||||
self.econtent = (nacl.public.SealedBox(self.sfrom.pkey).encrypt(key) +
|
||||
nacl.public.SealedBox(self.sto.pkey).encrypt(key) +
|
||||
nacl.secret.SecretBox(key).encrypt(scontent))
|
||||
|
||||
def sealed(self) -> 'Message':
|
||||
self.seal()
|
||||
return self
|
||||
|
||||
def dumps(self) -> str:
|
||||
return json.dumps({
|
||||
'sf': self.sfrom.dumps(),
|
||||
'st': self.sto.dumps(),
|
||||
'in': Encoding.encode(self.idnonce),
|
||||
'ts': self.timestamp,
|
||||
'en': Encoding.encode(self.editnonce),
|
||||
'ec': Encoding.encode(self.econtent),
|
||||
'fl': self.flags,
|
||||
})
|
||||
|
||||
def unseal(self, s: int) -> bytes:
|
||||
scontent = self.decrypt(s)
|
||||
self.pcontent = self.sfrom.vkey.verify(scontent)
|
||||
return self.pcontent
|
||||
|
||||
def decrypt(self, s: int) -> bytes:
|
||||
subject = self.pair[s]
|
||||
assert isinstance(subject, PrivateSubject)
|
||||
key: bytes = nacl.public.SealedBox(subject.ekey).decrypt(
|
||||
[self.econtent[:KEY_SIZE], self.econtent[KEY_SIZE:2 * KEY_SIZE]][s])
|
||||
return nacl.secret.SecretBox(key).decrypt(self.econtent[2 * KEY_SIZE:])
|
||||
|
||||
def unsealed(self, s: int):
|
||||
self.unseal(s)
|
||||
return self
|
||||
|
||||
def edit(self, pcontent: bytes) -> 'Message':
|
||||
return Message(self.sfrom, self.sto, self.idnonce, None,
|
||||
Encoding.nonce(), pcontent, None,
|
||||
self.flags.replace('<unedited>', '<edited>')).sealed()
|
||||
|
||||
def edited(self, other: 'Message'):
|
||||
return self.pair == other.pair and self.idnonce == other.idnonce
|
||||
|
||||
def editt(self, content: bytes) -> Tuple['Message', 'Message']:
|
||||
return self, self.edit(content)
|
||||
|
||||
def delete(self):
|
||||
return Message(self.sfrom, self.sto, self.idnonce, None,
|
||||
None, None, None,
|
||||
'')
|
||||
|
||||
def flags_(self, flags: str):
|
||||
return Message(self.sfrom, self.sto, self.idnonce, None,
|
||||
self.editnonce, None, None,
|
||||
flags)
|
||||
|
||||
def alter(self, sfrom: Optional[Subject] = None, sto: Optional[Subject] = None):
|
||||
if sfrom is not None:
|
||||
assert self.sfrom == sfrom
|
||||
self.sfrom = sfrom
|
||||
if sto is not None:
|
||||
assert self.sto == sto
|
||||
self.sto = sto
|
||||
return self
|
39
v25/messaging/subject.py
Normal file
39
v25/messaging/subject.py
Normal file
@ -0,0 +1,39 @@
|
||||
import nacl.public
|
||||
import nacl.pwhash
|
||||
import nacl.signing
|
||||
|
||||
from v25.messaging.encoding import Encoding
|
||||
|
||||
|
||||
class Subject:
|
||||
def __init__(self, vkey: bytes):
|
||||
self.vkey = nacl.signing.VerifyKey(vkey)
|
||||
self.pkey: nacl.public.PublicKey = self.vkey.to_curve25519_public_key()
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Subject):
|
||||
return False
|
||||
return self.vkey == other.vkey
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.vkey.encode())
|
||||
|
||||
def dumps(self) -> str:
|
||||
return Encoding.encode(self.vkey)
|
||||
|
||||
@classmethod
|
||||
def loads(cls, s: str):
|
||||
return Subject(Encoding.decode(s))
|
||||
|
||||
|
||||
class PrivateSubject(Subject):
|
||||
def __init__(self, pwd: str):
|
||||
salt = nacl.pwhash.argon2id.SALTBYTES * b'\0'
|
||||
self.seed = nacl.pwhash.argon2id.kdf(
|
||||
nacl.public.PrivateKey.SEED_SIZE, pwd.encode(), salt,
|
||||
|
||||
nacl.pwhash.OPSLIMIT_INTERACTIVE,
|
||||
nacl.pwhash.MEMLIMIT_INTERACTIVE)
|
||||
self.skey = nacl.signing.SigningKey(self.seed)
|
||||
self.ekey: nacl.public.PrivateKey = self.skey.to_curve25519_private_key()
|
||||
super().__init__(bytes(self.skey.verify_key))
|
0
v25/storage/__init__.py
Normal file
0
v25/storage/__init__.py
Normal file
135
v25/storage/dbstorage.py
Normal file
135
v25/storage/dbstorage.py
Normal file
@ -0,0 +1,135 @@
|
||||
from typing import Tuple, Optional, Iterable
|
||||
|
||||
from nacl.bindings import crypto_sign_PUBLICKEYBYTES
|
||||
from sqlalchemy import create_engine, LargeBinary, Column, REAL, BLOB, String, or_, and_, BigInteger
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Query
|
||||
|
||||
from v25.messaging.encoding import NONCE_SIZE, Encoding
|
||||
from v25.messaging.message import Message
|
||||
from v25.messaging.subject import Subject
|
||||
from v25.storage.storage import AbstractStorage
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class MsgSgn(Base):
|
||||
__tablename__ = 'msgsgns'
|
||||
|
||||
sf = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
st = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
idn = Column(LargeBinary(NONCE_SIZE), primary_key=True)
|
||||
|
||||
|
||||
class Msg(Base):
|
||||
__tablename__ = 'msgs'
|
||||
|
||||
sf = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
st = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True)
|
||||
idn = Column(LargeBinary(NONCE_SIZE), primary_key=True)
|
||||
ts = Column(REAL, nullable=False, index=True)
|
||||
en = Column(LargeBinary(NONCE_SIZE), nullable=False)
|
||||
ec = Column(BLOB, nullable=False)
|
||||
flags = Column(String, nullable=False)
|
||||
oid = Column(BigInteger, autoincrement=True, index=True, unique=True, nullable=False)
|
||||
|
||||
def sgn(self):
|
||||
return MsgSgn(sf=self.sf, st=self.st, idn=self.idn)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{type(self).__name__}(" \
|
||||
f"sf={self.sf!r}, st={self.st!r}, idn={self.idn!r}, ts={self.ts!r}," \
|
||||
f" en={self.en!r}, ec={self.ec!r}," \
|
||||
f" flags={self.flags!r}" \
|
||||
f")"
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, m: Message):
|
||||
# noinspection PyArgumentList
|
||||
return cls(sf=m.sfrom.vkey.encode(), st=m.sto.vkey.encode(), idn=m.idnonce, ts=m.timestamp,
|
||||
en=m.editnonce, ec=m.econtent,
|
||||
flags=m.flags)
|
||||
|
||||
def to_message(self):
|
||||
return Message(Subject(self.sf), Subject(self.st), self.idn, self.ts,
|
||||
self.en, None, self.ec, self.flags)
|
||||
|
||||
|
||||
class DBStorage(AbstractStorage):
|
||||
def __init__(self, *args):
|
||||
self.engine = create_engine(*args, echo=False)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
def check(self, s: Subject) -> dict:
|
||||
return {'allowed': None}
|
||||
|
||||
def push(self, m: Message) -> None:
|
||||
session = self.Session()
|
||||
msg = Msg.from_message(m)
|
||||
session.add(msg)
|
||||
session.add(msg.sgn())
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def edit(self, old: Message, new: Message):
|
||||
assert old.edited(new)
|
||||
session = self.Session()
|
||||
msg = self.one_alike(session, old)
|
||||
assert msg.en == old.editnonce
|
||||
msgn = Msg.from_message(new)
|
||||
msg.en = msgn.en
|
||||
msg.ec = msgn.ec
|
||||
msg.flags = msgn.flags
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
@staticmethod
|
||||
def one_alike(session, m: Message) -> Msg:
|
||||
return session.query(Msg).filter_by(sf=m.sfrom.vkey.encode(), st=m.sto.vkey.encode(), idn=m.idnonce).one()
|
||||
|
||||
def delete(self, m: Message):
|
||||
session = self.Session()
|
||||
session.delete(self.one_alike(session, m))
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
if params is None:
|
||||
params = {}
|
||||
session = self.Session()
|
||||
query: Query = session.query(Msg).filter(or_(
|
||||
and_(
|
||||
Msg.sf == pair[0].vkey.encode(),
|
||||
Msg.st == pair[1].vkey.encode(),
|
||||
),
|
||||
and_(
|
||||
Msg.sf == pair[1].vkey.encode(),
|
||||
Msg.st == pair[0].vkey.encode(),
|
||||
),
|
||||
))
|
||||
if 'ts' in params:
|
||||
if '>' in params:
|
||||
query = query.filter(Msg.ts > params['ts']['>'])
|
||||
if '<' in params:
|
||||
query = query.filter(Msg.ts < params['ts']['<'])
|
||||
if params.get('before'):
|
||||
query = query.filter(Msg.oid < self.one_alike(
|
||||
session,
|
||||
Message(pair[0], pair[1], Encoding.decode(params['before']), None,
|
||||
None, None, None,
|
||||
'')).oid)
|
||||
for flag in params.get('flags', ()):
|
||||
query = query.filter(Msg.flags.contains(flag))
|
||||
query = query.order_by(Msg.oid.desc())
|
||||
if 'limit' in params:
|
||||
query = query.limit(params['limit'])
|
||||
return map(Msg.to_message, query.from_self().order_by(Msg.oid))
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
session = self.Session()
|
||||
msg: Msg = self.one_alike(session, m)
|
||||
assert msg.en == m.editnonce
|
||||
msg.flags = flags
|
||||
session.commit()
|
||||
session.close()
|
35
v25/storage/secstorage.py
Normal file
35
v25/storage/secstorage.py
Normal file
@ -0,0 +1,35 @@
|
||||
from typing import Tuple, Optional, Iterable
|
||||
|
||||
from v25.messaging.message import Message
|
||||
from v25.messaging.subject import Subject
|
||||
from v25.storage.storage import AbstractStorage
|
||||
|
||||
|
||||
class SecureStorage(AbstractStorage):
|
||||
def __init__(self, storage: AbstractStorage, subject: Subject):
|
||||
self.storage = storage
|
||||
self.subject = subject
|
||||
|
||||
def check(self, s: Subject) -> dict:
|
||||
assert self.subject == s
|
||||
return self.storage.check(s)
|
||||
|
||||
def push(self, m: Message) -> None:
|
||||
assert self.subject == m.sfrom
|
||||
return self.storage.push(m)
|
||||
|
||||
def edit(self, old: Message, new: Message):
|
||||
assert self.subject == old.sfrom
|
||||
return self.storage.edit(old, new)
|
||||
|
||||
def delete(self, m: Message):
|
||||
assert self.subject in m.pair
|
||||
return self.storage.delete(m)
|
||||
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
assert self.subject in pair
|
||||
return self.storage.pull(pair, params)
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
assert self.subject in m.pair
|
||||
return self.storage.flags(m, flags)
|
25
v25/storage/storage.py
Normal file
25
v25/storage/storage.py
Normal file
@ -0,0 +1,25 @@
|
||||
from abc import ABC
|
||||
from typing import Tuple, Optional, List, Iterable
|
||||
|
||||
from v25.messaging.message import Message
|
||||
from v25.messaging.subject import Subject
|
||||
|
||||
|
||||
class AbstractStorage(ABC):
|
||||
def check(self, s: Subject) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def push(self, m: Message) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def edit(self, old: Message, new: Message):
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, m: Message):
|
||||
raise NotImplementedError
|
||||
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
raise NotImplementedError
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
raise NotImplementedError
|
0
v25/web/__init__.py
Normal file
0
v25/web/__init__.py
Normal file
0
v25/web/client/__init__.py
Normal file
0
v25/web/client/__init__.py
Normal file
46
v25/web/client/remotestorage.py
Normal file
46
v25/web/client/remotestorage.py
Normal file
@ -0,0 +1,46 @@
|
||||
from json import dumps
|
||||
from typing import Tuple, Optional, Iterable
|
||||
|
||||
import requests
|
||||
|
||||
from v25.messaging.encoding import Encoding
|
||||
from v25.messaging.message import Message
|
||||
from v25.messaging.subject import Subject, PrivateSubject
|
||||
from v25.storage.storage import AbstractStorage
|
||||
|
||||
|
||||
class RemoteStorage(AbstractStorage):
|
||||
def __init__(self, url: str, subject: PrivateSubject):
|
||||
self.url = url
|
||||
self.subject = subject
|
||||
|
||||
def api(self, s: str):
|
||||
return f'{self.url}/{s}'
|
||||
|
||||
def req(self, api: str, o: object):
|
||||
source = dumps(o)
|
||||
req = requests.post(self.api(api), json={
|
||||
'source': source,
|
||||
'subject': self.subject.dumps(),
|
||||
'signature': Encoding.encode(self.subject.skey.sign(source.encode()).signature),
|
||||
})
|
||||
return req.json()
|
||||
|
||||
def check(self, s: Subject) -> dict:
|
||||
return self.req('check', s.dumps())
|
||||
|
||||
def push(self, m: Message) -> None:
|
||||
self.req('push', m.dumps())
|
||||
|
||||
def edit(self, old: Message, new: Message):
|
||||
self.req('edit', {'old': old.dumps(), 'new': new.dumps()})
|
||||
|
||||
def delete(self, m: Message):
|
||||
self.req('delete', m.dumps())
|
||||
|
||||
def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]:
|
||||
return map(Message.loads,
|
||||
self.req('pull', {'params': params, 'pair': [subject.dumps() for subject in pair]}))
|
||||
|
||||
def flags(self, m: Message, flags: str):
|
||||
self.req('flags', {'m': m.dumps(), 'flags': flags})
|
0
v25/web/server/__init__.py
Normal file
0
v25/web/server/__init__.py
Normal file
66
v25/web/server/api.py
Normal file
66
v25/web/server/api.py
Normal file
@ -0,0 +1,66 @@
|
||||
from json import loads
|
||||
from typing import Tuple
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
from v25.messaging.encoding import Encoding
|
||||
from v25.messaging.message import Message
|
||||
from v25.messaging.subject import Subject
|
||||
from v25.storage.secstorage import SecureStorage
|
||||
from v25.storage.storage import AbstractStorage
|
||||
|
||||
|
||||
class API(Flask):
|
||||
def __init__(self, import_name, storage: AbstractStorage):
|
||||
self.storage = storage
|
||||
super().__init__(import_name)
|
||||
self.routes()
|
||||
|
||||
def allowed(self, s: Subject):
|
||||
return 'allowed' in self.storage.check(s)
|
||||
|
||||
def ss(self):
|
||||
d = request.json
|
||||
source: str = d['source']
|
||||
subject = Subject.loads(d['subject'])
|
||||
assert self.allowed(subject)
|
||||
subject.vkey.verify(source.encode(), Encoding.decode(d['signature']))
|
||||
return loads(source), SecureStorage(self.storage, subject)
|
||||
|
||||
def routes(self):
|
||||
app = self
|
||||
|
||||
@app.route('/')
|
||||
def root():
|
||||
return "V25API"
|
||||
|
||||
@app.route('/check', methods=['POST'])
|
||||
def check():
|
||||
d, storage = self.ss()
|
||||
return jsonify(storage.check(Subject.loads(d)))
|
||||
|
||||
@app.route('/push', methods=['POST'])
|
||||
def push():
|
||||
d, storage = self.ss()
|
||||
return jsonify(storage.push(Message.loads(d)))
|
||||
|
||||
@app.route('/edit', methods=['POST'])
|
||||
def edit():
|
||||
d, storage = self.ss()
|
||||
return jsonify(storage.edit(Message.loads(d['old']), Message.loads(d['new'])))
|
||||
|
||||
@app.route('/delete', methods=['POST'])
|
||||
def delete():
|
||||
d, storage = self.ss()
|
||||
return jsonify(storage.delete(Message.loads(d)))
|
||||
|
||||
@app.route('/pull', methods=['POST'])
|
||||
def pull():
|
||||
d, storage = self.ss()
|
||||
pair: Tuple[Subject, Subject] = Subject.loads(d['pair'][0]), Subject.loads(d['pair'][1])
|
||||
return jsonify(list(map(Message.dumps, storage.pull(pair, d['params']))))
|
||||
|
||||
@app.route('/flags', methods=['POST'])
|
||||
def flags():
|
||||
d, storage = self.ss()
|
||||
return jsonify(storage.flags(Message.loads(d['m']), d['flags']))
|
Loading…
Reference in New Issue
Block a user