mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-22 06:27:59 +00:00
add aputils module for hs2019 support
This commit is contained in:
parent
130111c847
commit
5d01211a34
|
@ -35,6 +35,7 @@ class Application(web.Application):
|
||||||
self['database'].load()
|
self['database'].load()
|
||||||
|
|
||||||
self['client'] = HttpClient(
|
self['client'] = HttpClient(
|
||||||
|
database = self.database,
|
||||||
limit = self.config.push_limit,
|
limit = self.config.push_limit,
|
||||||
timeout = self.config.timeout,
|
timeout = self.config.timeout,
|
||||||
cache_size = self.config.json_cache
|
cache_size = self.config.json_cache
|
||||||
|
@ -155,6 +156,7 @@ class PushWorker(threading.Thread):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.client = HttpClient(
|
self.client = HttpClient(
|
||||||
|
database = self.app.database,
|
||||||
limit = self.app.config.push_limit,
|
limit = self.app.config.push_limit,
|
||||||
timeout = self.app.config.timeout,
|
timeout = self.app.config.timeout,
|
||||||
cache_size = self.app.config.json_cache
|
cache_size = self.app.config.json_cache
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import aputils
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,22 +17,7 @@ class RelayDatabase(dict):
|
||||||
})
|
})
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.PRIVKEY = None
|
self.signer = None
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def PUBKEY(self):
|
|
||||||
return self.PRIVKEY.publickey()
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pubkey(self):
|
|
||||||
return self.PUBKEY.exportKey('PEM').decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def privkey(self):
|
|
||||||
return self['private-key']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -45,11 +30,6 @@ class RelayDatabase(dict):
|
||||||
return tuple(data['inbox'] for data in self['relay-list'].values())
|
return tuple(data['inbox'] for data in self['relay-list'].values())
|
||||||
|
|
||||||
|
|
||||||
def generate_key(self):
|
|
||||||
self.PRIVKEY = RSA.generate(4096)
|
|
||||||
self['private-key'] = self.PRIVKEY.exportKey('PEM').decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
new_db = True
|
new_db = True
|
||||||
|
|
||||||
|
@ -94,12 +74,13 @@ class RelayDatabase(dict):
|
||||||
if self.config.db.stat().st_size > 0:
|
if self.config.db.stat().st_size > 0:
|
||||||
raise e from None
|
raise e from None
|
||||||
|
|
||||||
if not self.privkey:
|
if not self['private-key']:
|
||||||
logging.info("No actor keys present, generating 4096-bit RSA keypair.")
|
logging.info("No actor keys present, generating 4096-bit RSA keypair.")
|
||||||
self.generate_key()
|
self.signer = aputils.Signer.new(self.config.keyid, 4096)
|
||||||
|
self['private-key'] = self.signer.export()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.PRIVKEY = RSA.importKey(self.privkey)
|
self.signer = aputils.Signer(self['private-key'], self.config.keyid)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
return not new_db
|
return not new_db
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import aputils
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -12,9 +13,7 @@ from . import __version__
|
||||||
from .misc import (
|
from .misc import (
|
||||||
MIMETYPES,
|
MIMETYPES,
|
||||||
DotDict,
|
DotDict,
|
||||||
Message,
|
Message
|
||||||
create_signature_header,
|
|
||||||
generate_body_digest
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +29,8 @@ class Cache(LRUCache):
|
||||||
|
|
||||||
|
|
||||||
class HttpClient:
|
class HttpClient:
|
||||||
def __init__(self, limit=100, timeout=10, cache_size=1024):
|
def __init__(self, database, limit=100, timeout=10, cache_size=1024):
|
||||||
|
self.database = database
|
||||||
self.cache = Cache(cache_size)
|
self.cache = Cache(cache_size)
|
||||||
self.cfg = {'limit': limit, 'timeout': timeout}
|
self.cfg = {'limit': limit, 'timeout': timeout}
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
@ -47,29 +47,6 @@ class HttpClient:
|
||||||
return self.cfg['timeout']
|
return self.cfg['timeout']
|
||||||
|
|
||||||
|
|
||||||
def sign_headers(self, method, url, message=None):
|
|
||||||
parsed = urlparse(url)
|
|
||||||
headers = {
|
|
||||||
'(request-target)': f'{method.lower()} {parsed.path}',
|
|
||||||
'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
|
||||||
'Host': parsed.netloc
|
|
||||||
}
|
|
||||||
|
|
||||||
if message:
|
|
||||||
data = message.to_json()
|
|
||||||
headers.update({
|
|
||||||
'Digest': f'SHA-256={generate_body_digest(data)}',
|
|
||||||
'Content-Length': str(len(data.encode('utf-8')))
|
|
||||||
})
|
|
||||||
|
|
||||||
headers['Signature'] = create_signature_header(headers)
|
|
||||||
|
|
||||||
del headers['(request-target)']
|
|
||||||
del headers['Host']
|
|
||||||
|
|
||||||
return headers
|
|
||||||
|
|
||||||
|
|
||||||
async def open(self):
|
async def open(self):
|
||||||
if self._session:
|
if self._session:
|
||||||
return
|
return
|
||||||
|
@ -110,7 +87,7 @@ class HttpClient:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
if sign_headers:
|
if sign_headers:
|
||||||
headers.update(self.sign_headers('GET', url))
|
headers.update(self.database.signer.sign_headers('GET', url))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.verbose(f'Fetching resource: {url}')
|
logging.verbose(f'Fetching resource: {url}')
|
||||||
|
@ -162,8 +139,19 @@ class HttpClient:
|
||||||
async def post(self, url, message):
|
async def post(self, url, message):
|
||||||
await self.open()
|
await self.open()
|
||||||
|
|
||||||
|
instance = self.database.get_inbox(url)
|
||||||
|
|
||||||
|
## Akkoma (and probably pleroma) doesn't support hs2019, so use the old algorithm
|
||||||
|
if instance.get('software') in {'akkoma', 'pleroma'}:
|
||||||
|
algorithm = aputils.Algorithm.RSASHA256
|
||||||
|
|
||||||
|
else:
|
||||||
|
algorithm = aputils.Algorithm.HS2019
|
||||||
|
|
||||||
headers = {'Content-Type': 'application/activity+json'}
|
headers = {'Content-Type': 'application/activity+json'}
|
||||||
headers.update(self.sign_headers('POST', url, message))
|
headers.update(self.database.signer.sign_headers('POST', url, message, algorithm=algorithm))
|
||||||
|
|
||||||
|
print(headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.verbose(f'Sending "{message.type}" to {url}')
|
logging.verbose(f'Sending "{message.type}" to {url}')
|
||||||
|
@ -185,7 +173,7 @@ class HttpClient:
|
||||||
|
|
||||||
|
|
||||||
## Additional methods ##
|
## Additional methods ##
|
||||||
async def fetch_nodeinfo(domain):
|
async def fetch_nodeinfo(self, domain):
|
||||||
nodeinfo_url = None
|
nodeinfo_url = None
|
||||||
wk_nodeinfo = await self.get(f'https://{domain}/.well-known/nodeinfo', loads=WKNodeinfo)
|
wk_nodeinfo = await self.get(f'https://{domain}/.well-known/nodeinfo', loads=WKNodeinfo)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import aputils
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
@ -6,9 +7,6 @@ import socket
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from Crypto.Hash import SHA, SHA256, SHA512
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
|
||||||
from aiohttp.hdrs import METH_ALL as METHODS
|
from aiohttp.hdrs import METH_ALL as METHODS
|
||||||
from aiohttp.web import Response as AiohttpResponse, View as AiohttpView
|
from aiohttp.web import Response as AiohttpResponse, View as AiohttpView
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -21,12 +19,6 @@ from .http_debug import http_debug
|
||||||
|
|
||||||
app = None
|
app = None
|
||||||
|
|
||||||
HASHES = {
|
|
||||||
'sha1': SHA,
|
|
||||||
'sha256': SHA256,
|
|
||||||
'sha512': SHA512
|
|
||||||
}
|
|
||||||
|
|
||||||
MIMETYPES = {
|
MIMETYPES = {
|
||||||
'activity': 'application/activity+json',
|
'activity': 'application/activity+json',
|
||||||
'html': 'text/html',
|
'html': 'text/html',
|
||||||
|
@ -92,67 +84,12 @@ def check_open_port(host, port):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_signature_header(headers):
|
|
||||||
headers = {k.lower(): v for k, v in headers.items()}
|
|
||||||
used_headers = headers.keys()
|
|
||||||
sigstring = build_signing_string(headers, used_headers)
|
|
||||||
|
|
||||||
sig = {
|
|
||||||
'keyId': app.config.keyid,
|
|
||||||
'algorithm': 'rsa-sha256',
|
|
||||||
'headers': ' '.join(used_headers),
|
|
||||||
'signature': sign_signing_string(sigstring, app.database.PRIVKEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
|
|
||||||
return ','.join(chunks)
|
|
||||||
|
|
||||||
|
|
||||||
def distill_inboxes(actor, object_id):
|
def distill_inboxes(actor, object_id):
|
||||||
for inbox in app.database.inboxes:
|
for inbox in app.database.inboxes:
|
||||||
if inbox != actor.shared_inbox and urlparse(inbox).hostname != urlparse(object_id).hostname:
|
if inbox != actor.shared_inbox and urlparse(inbox).hostname != urlparse(object_id).hostname:
|
||||||
yield inbox
|
yield inbox
|
||||||
|
|
||||||
|
|
||||||
def generate_body_digest(body):
|
|
||||||
h = SHA256.new(body.encode('utf-8'))
|
|
||||||
bodyhash = base64.b64encode(h.digest()).decode('utf-8')
|
|
||||||
|
|
||||||
return bodyhash
|
|
||||||
|
|
||||||
|
|
||||||
def sign_signing_string(sigstring, key):
|
|
||||||
pkcs = PKCS1_v1_5.new(key)
|
|
||||||
h = SHA256.new()
|
|
||||||
h.update(sigstring.encode('ascii'))
|
|
||||||
sigdata = pkcs.sign(h)
|
|
||||||
|
|
||||||
return base64.b64encode(sigdata).decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_signature(actor, signature, http_request):
|
|
||||||
headers = {key.lower(): value for key, value in http_request.headers.items()}
|
|
||||||
headers['(request-target)'] = ' '.join([http_request.method.lower(), http_request.path])
|
|
||||||
|
|
||||||
sigstring = build_signing_string(headers, signature['headers'])
|
|
||||||
logging.debug(f'sigstring: {sigstring}')
|
|
||||||
|
|
||||||
sign_alg, _, hash_alg = signature['algorithm'].partition('-')
|
|
||||||
logging.debug(f'sign alg: {sign_alg}, hash alg: {hash_alg}')
|
|
||||||
|
|
||||||
sigdata = base64.b64decode(signature['signature'])
|
|
||||||
|
|
||||||
pkcs = PKCS1_v1_5.new(actor.PUBKEY)
|
|
||||||
h = HASHES[hash_alg].new()
|
|
||||||
h.update(sigstring.encode('ascii'))
|
|
||||||
result = pkcs.verify(h, sigdata)
|
|
||||||
|
|
||||||
http_request['validated'] = result
|
|
||||||
|
|
||||||
logging.debug(f'validates? {result}')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class DotDict(dict):
|
class DotDict(dict):
|
||||||
def __init__(self, _data, **kwargs):
|
def __init__(self, _data, **kwargs):
|
||||||
dict.__init__(self)
|
dict.__init__(self)
|
||||||
|
@ -321,16 +258,6 @@ class Message(DotDict):
|
||||||
|
|
||||||
|
|
||||||
# actor properties
|
# actor properties
|
||||||
@property
|
|
||||||
def PUBKEY(self):
|
|
||||||
return RSA.import_key(self.pubkey)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pubkey(self):
|
|
||||||
return self.publicKey.publicKeyPem
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shared_inbox(self):
|
def shared_inbox(self):
|
||||||
return self.get('endpoints', {}).get('sharedInbox', self.inbox)
|
return self.get('endpoints', {}).get('sharedInbox', self.inbox)
|
||||||
|
@ -353,6 +280,11 @@ class Message(DotDict):
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signer(self):
|
||||||
|
return aputils.Signer.new_from_actor(self)
|
||||||
|
|
||||||
|
|
||||||
class Nodeinfo(DotDict):
|
class Nodeinfo(DotDict):
|
||||||
@property
|
@property
|
||||||
def swname(self):
|
def swname(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import aputils
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -66,7 +67,7 @@ a:hover {{ color: #8AF; }}
|
||||||
async def actor(request):
|
async def actor(request):
|
||||||
data = Message.new_actor(
|
data = Message.new_actor(
|
||||||
host = request.config.host,
|
host = request.config.host,
|
||||||
pubkey = request.database.pubkey
|
pubkey = request.database.signer.pubkey
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response.new(data, ctype='activity')
|
return Response.new(data, ctype='activity')
|
||||||
|
@ -127,9 +128,13 @@ async def inbox(request):
|
||||||
return Response.new_error(403, 'access denied', 'json')
|
return Response.new_error(403, 'access denied', 'json')
|
||||||
|
|
||||||
## reject if the signature is invalid
|
## reject if the signature is invalid
|
||||||
if not (await misc.validate_signature(request.actor, request.signature, request)):
|
try:
|
||||||
|
await request.actor.signer.validate_aiohttp_request(request)
|
||||||
|
|
||||||
|
except aputils.SignatureValidationError as e:
|
||||||
logging.verbose(f'signature validation failed for: {actor.id}')
|
logging.verbose(f'signature validation failed for: {actor.id}')
|
||||||
return Response.new_error(401, 'signature check failed', 'json')
|
logging.debug(str(e))
|
||||||
|
return Response.new_error(401, str(e), 'json')
|
||||||
|
|
||||||
## reject if activity type isn't 'Follow' and the actor isn't following
|
## reject if activity type isn't 'Follow' and the actor isn't following
|
||||||
if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain):
|
if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain):
|
||||||
|
|
|
@ -28,6 +28,7 @@ install_requires =
|
||||||
click >= 8.1.2
|
click >= 8.1.2
|
||||||
pycryptodome >= 3.14.1
|
pycryptodome >= 3.14.1
|
||||||
PyYAML >= 5.0.0
|
PyYAML >= 5.0.0
|
||||||
|
aputils @ https://git.barkshark.xyz/barkshark/aputils/archive/0.1.1.tar.gz
|
||||||
python_requires = >=3.6
|
python_requires = >=3.6
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
|
|
Loading…
Reference in a new issue