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
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.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