Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon

merge-requests/30/head
Bob Mottram 2021-01-04 21:35:58 +00:00
commit 96fe9f0513
28 changed files with 770 additions and 234 deletions

View File

@ -177,6 +177,7 @@ from shares import addShare
from shares import removeShare
from shares import expireShares
from categories import setHashtagCategory
from utils import getLockedAccount
from utils import hasUsersPath
from utils import getFullDomain
from utils import removeHtml
@ -1078,6 +1079,8 @@ class PubServer(BaseHTTPRequestHandler):
elif self.headers.get('content-length'):
headersDict['content-length'] = self.headers['content-length']
originalMessageJson = messageJson.copy()
# For follow activities add a 'to' field, which is a copy
# of the object field
messageJson, toFieldExists = \
@ -1096,7 +1099,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.httpPrefix,
nickname,
self.server.domainFull,
messageJson,
messageJson, originalMessageJson,
messageBytesDecoded,
headersDict,
self.path,
@ -5214,11 +5217,13 @@ class PubServer(BaseHTTPRequestHandler):
jamiAddress = None
ssbAddress = None
emailAddress = None
lockedAccount = False
actorJson = getPersonFromCache(baseDir,
optionsActor,
self.server.personCache,
True)
if actorJson:
lockedAccount = getLockedAccount(actorJson)
donateUrl = getDonationUrl(actorJson)
xmppAddress = getXmppAddress(actorJson)
matrixAddress = getMatrixAddress(actorJson)
@ -5247,7 +5252,8 @@ class PubServer(BaseHTTPRequestHandler):
PGPpubKey, PGPfingerprint,
emailAddress,
self.server.dormantMonths,
backToPath).encode('utf-8')
backToPath,
lockedAccount).encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
cookie, callingDomain)
@ -13229,6 +13235,12 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.debug:
print('DEBUG: Check message has params')
if not messageJson:
self.send_response(403)
self.end_headers()
self.server.POSTbusy = False
return
if self.path.endswith('/inbox') or \
self.path == '/sharedInbox':
if not inboxMessageHasParams(messageJson):

View File

@ -11,7 +11,7 @@
# License
# =======
#
# Copyright (C) 2020 Bob Mottram <bob@freedombone.net>
# Copyright (C) 2020-2021 Bob Mottram <bob@freedombone.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -478,7 +478,6 @@ if args.debug:
if args.tests:
runAllTests()
sys.exit()
if args.testsnetwork:
print('Network Tests')
testPostMessageBetweenServers()

View File

@ -175,8 +175,8 @@ def followerOfPerson(baseDir: str, nickname: str, domain: str,
federationList, debug, 'followers.txt')
def _isFollowerOfPerson(baseDir: str, nickname: str, domain: str,
followerNickname: str, followerDomain: str) -> bool:
def isFollowerOfPerson(baseDir: str, nickname: str, domain: str,
followerNickname: str, followerDomain: str) -> bool:
"""is the given nickname a follower of followerNickname?
"""
if ':' in domain:
@ -663,9 +663,9 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
baseDir + '/accounts/' + handleToFollow)
return True
if _isFollowerOfPerson(baseDir,
nicknameToFollow, domainToFollowFull,
nickname, domainFull):
if isFollowerOfPerson(baseDir,
nicknameToFollow, domainToFollowFull,
nickname, domainFull):
if debug:
print('DEBUG: ' + nickname + '@' + domain +
' is already a follower of ' +

View File

@ -10,6 +10,7 @@ import json
import os
import datetime
import time
from linked_data_sig import verifyJsonSignature
from utils import hasUsersPath
from utils import validPostDate
from utils import getFullDomain
@ -312,6 +313,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
def savePostToInboxQueue(baseDir: str, httpPrefix: str,
nickname: str, domain: str,
postJsonObject: {},
originalPostJsonObject: {},
messageBytes: str,
httpHeaders: {},
postPath: str, debug: bool) -> str:
@ -436,6 +438,7 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
'httpHeaders': httpHeaders,
'path': postPath,
'post': postJsonObject,
'original': originalPostJsonObject,
'digest': digest,
'filename': filename,
'destination': destination
@ -2679,9 +2682,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
queue.pop(0)
continue
# check the signature
# check the http header signature
if debug:
print('DEBUG: checking http headers')
print('DEBUG: checking http header signature')
pprint(queueJson['httpHeaders'])
postStr = json.dumps(queueJson['post'])
if not verifyPostHeaders(httpPrefix,
@ -2700,7 +2703,37 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
continue
if debug:
print('DEBUG: Signature check success')
print('DEBUG: http header signature check success')
# check if a json signature exists on this post
checkJsonSignature = False
if queueJson['original'].get('@context') and \
queueJson['original'].get('signature'):
if isinstance(queueJson['original']['signature'], dict):
# see https://tools.ietf.org/html/rfc7515
jwebsig = queueJson['original']['signature']
# signature exists and is of the expected type
if jwebsig.get('type') and jwebsig.get('signatureValue'):
if jwebsig['type'] == 'RsaSignature2017':
checkJsonSignature = True
if checkJsonSignature:
# use the original json message received, not one which may have
# been modified along the way
if not verifyJsonSignature(queueJson['original'], pubKey):
if debug:
print('WARN: jsonld inbox signature check failed ' +
keyId + ' ' + pubKey + ' ' +
str(queueJson['original']))
else:
print('WARN: jsonld inbox signature check failed ' +
keyId)
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
continue
else:
print('jsonld inbox signature check success ' + keyId)
# set the id to the same as the post filename
# This makes the filename and the id consistent

View File

@ -1,155 +0,0 @@
__filename__ = "jsonldsig.py"
__author__ = "Bob Mottram"
__credits__ = ['Based on ' +
'https://github.com/WebOfTrustInfo/ld-signatures-python']
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
from copy import deepcopy
from datetime import datetime
import pytz
try:
from Cryptodome.PublicKey import RSA
from Cryptodome.Hash import SHA256
from Cryptodome.Signature import pkcs1_5 as PKCS1_v1_5
except ImportError:
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from pyjsonld import normalize
import base64
import json
def _b64safeEncode(payload: {}) -> str:
"""
b64 url safe encoding with the padding removed.
"""
return base64.urlsafe_b64encode(payload).rstrip(b'=')
def _b64safeDecode(payload: {}) -> str:
"""
b64 url safe decoding with the padding added.
"""
return base64.urlsafe_b64decode(payload + b'=' * (4 - len(payload) % 4))
def _normalizeJson(payload: {}) -> str:
"""
Normalize with URDNA2015
"""
return json.dumps(payload, separators=(',', ':'),
sort_keys=True).encode('utf-8')
def _signRs256(payload: {}, privateKeyPem: str) -> str:
"""
Produce a RS256 signature of the payload
"""
key = RSA.importKey(privateKeyPem)
signer = PKCS1_v1_5.new(key)
signature = signer.sign(SHA256.new(payload))
return signature
def _verifyRs256(payload: {}, signature: str, publicKeyPem: str) -> bool:
"""
Verifies a RS256 signature
"""
key = RSA.importKey(publicKeyPem)
verifier = PKCS1_v1_5.new(key)
return verifier.verify(SHA256.new(payload), signature)
def _signJws(payload: {}, privateKeyPem: str) -> str:
"""
Prepare payload to sign
"""
header = {
'alg': 'RS256',
'b64': False,
'crit': ['b64']
}
normalizedJson = _normalizeJson(header)
encodedHeader = _b64safeEncode(normalizedJson)
preparedPayload = b'.'.join([encodedHeader, payload])
signature = _signRs256(preparedPayload, privateKeyPem)
encodedSignature = _b64safeEncode(signature)
jwsSignature = b'..'.join([encodedHeader, encodedSignature])
return jwsSignature
def _verifyJws(payload: {}, jwsSignature: str, publicKeyPem: str) -> bool:
"""
Verifies a signature using the given public key
"""
encodedHeader, encodedSignature = jwsSignature.split(b'..')
signature = _b64safeDecode(encodedSignature)
payload = b'.'.join([encodedHeader, payload])
return _verifyRs256(payload, signature, publicKeyPem)
def _jsonldNormalize(jldDocument: str):
"""
Normalize and hash the json-ld document
"""
options = {
'algorithm': 'URDNA2015',
'format': 'application/nquads'
}
normalized = normalize(jldDocument, options=options)
normalizedHash = SHA256.new(data=normalized.encode('utf-8')).digest()
return normalizedHash
def jsonldSign(jldDocument: {}, privateKeyPem: str) -> {}:
"""
Produces a signed JSON-LD document with a Json Web Signature
"""
jldDocument = deepcopy(jldDocument)
normalizedJldHash = _jsonldNormalize(jldDocument)
jwsSignature = _signJws(normalizedJldHash, privateKeyPem)
# construct the signature document and add it to jsonld
signature = {
'type': 'RsaSignatureSuite2017',
'created': datetime.now(tz=pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'signatureValue': jwsSignature.decode('utf-8')
}
jldDocument.update({'signature': signature})
return jldDocument
def jsonldVerify(signedJldDocument: {}, publicKeyPem: str) -> bool:
"""
Verifies the Json Web Signature of a signed JSON-LD Document
"""
signedJldDocument = deepcopy(signedJldDocument)
signature = signedJldDocument.pop('signature')
jwsSignature = signature['signatureValue'].encode('utf-8')
normalizedJldHash = _jsonldNormalize(signedJldDocument)
return _verifyJws(normalizedJldHash, jwsSignature, publicKeyPem)
def testSignJsonld(jldDocument: {}, privateKeyPem: str) -> {}:
"""
Creates a test signature
"""
signedJldDocument = jsonldSign(jldDocument, privateKeyPem)
# pop the created time key since its dynamic
signedJldDocument['signature'].pop('created')
return signedJldDocument

84
linked_data_sig.py 100644
View File

@ -0,0 +1,84 @@
__filename__ = "linked_data_sig.py"
__author__ = "Bob Mottram"
__credits__ = ['Based on ' +
'https://github.com/tsileo/little-boxes']
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import base64
import hashlib
from datetime import datetime
try:
from Cryptodome.PublicKey import RSA
from Cryptodome.Hash import SHA256
from Cryptodome.Signature import pkcs1_5 as PKCS1_v1_5
except ImportError:
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from pyjsonld import normalize
def _options_hash(doc):
doc = dict(doc["signature"])
for k in ["type", "id", "signatureValue"]:
if k in doc:
del doc[k]
doc["@context"] = "https://w3id.org/identity/v1"
options = {
"algorithm": "URDNA2015",
"format": "application/nquads"
}
normalized = normalize(doc, options)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()
def _doc_hash(doc):
doc = dict(doc)
if "signature" in doc:
del doc["signature"]
options = {
"algorithm": "URDNA2015",
"format": "application/nquads"
}
normalized = normalize(doc, options)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()
def verifyJsonSignature(doc: {}, publicKeyPem: str):
key = RSA.importKey(publicKeyPem)
to_be_signed = _options_hash(doc) + _doc_hash(doc)
signature = doc["signature"]["signatureValue"]
signer = PKCS1_v1_5.new(key) # type: ignore
digest = SHA256.new()
digest.update(to_be_signed.encode("utf-8"))
return signer.verify(digest, base64.b64decode(signature)) # type: ignore
def generateJsonSignature(doc: {}, privateKeyPem: str):
if not doc.get('actor'):
return
options = {
"type": "RsaSignature2017",
"creator": doc["actor"] + "#main-key",
"created": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
}
doc["signature"] = options
to_be_signed = _options_hash(doc) + _doc_hash(doc)
key = RSA.importKey(privateKeyPem)
signer = PKCS1_v1_5.new(key)
digest = SHA256.new()
digest.update(to_be_signed.encode("utf-8"))
sig = base64.b64encode(signer.sign(digest)) # type: ignore
options["signatureValue"] = sig.decode("utf-8")

View File

@ -65,7 +65,7 @@ from blocking import isBlocked
from blocking import isBlockedDomain
from filters import isFiltered
from git import convertPostToPatch
from jsonldsig import jsonldSign
from linked_data_sig import generateJsonSignature
from petnames import resolvePetnames
@ -1794,7 +1794,8 @@ def sendPost(projectVersion: str,
if not postJsonObject.get('signature'):
try:
signedPostJsonObject = jsonldSign(postJsonObject, privateKeyPem)
signedPostJsonObject = postJsonObject.copy()
generateJsonSignature(signedPostJsonObject, privateKeyPem)
postJsonObject = signedPostJsonObject
except Exception as e:
print('WARN: failed to JSON-LD sign post, ' + str(e))
@ -2122,7 +2123,8 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str,
if not postJsonObject.get('signature'):
try:
signedPostJsonObject = jsonldSign(postJsonObject, privateKeyPem)
signedPostJsonObject = postJsonObject.copy()
generateJsonSignature(signedPostJsonObject, privateKeyPem)
postJsonObject = signedPostJsonObject
except Exception as e:
print('WARN: failed to JSON-LD sign post, ' + str(e))

View File

@ -24,9 +24,7 @@ __all__ = [
'JsonLdProcessor', 'JsonLdError', 'ActiveContextCache']
import copy
import gzip
import hashlib
import io
import json
import os
import posixpath
@ -37,7 +35,6 @@ import string
import sys
import traceback
from collections import deque, namedtuple
from contextlib import closing
from numbers import Integral, Real
try:
@ -77,7 +74,6 @@ except ImportError:
# support python 2
if sys.version_info[0] >= 3:
from urllib.request import build_opener as urllib_build_opener
from urllib.request import HTTPSHandler
import urllib.parse as urllib_parse
from http.client import HTTPSConnection
@ -86,7 +82,6 @@ if sys.version_info[0] >= 3:
def cmp(a, b):
return (a > b) - (a < b)
else:
from urllib2 import build_opener as urllib_build_opener
from urllib2 import HTTPSHandler
import urlparse as urllib_parse
from httplib import HTTPSConnection
@ -234,7 +229,7 @@ def link(input_, ctx, options=None):
return frame(input, frame, options)
def normalize(input_, options=None):
def normalize(input_: {}, options=None):
"""
Performs JSON-LD normalization.
@ -355,6 +350,477 @@ def parse_link_header(header):
return rval
def getV1Schema() -> {}:
# https://w3id.org/identity/v1
return {
"@context": {
"id": "@id",
"type": "@type",
"cred": "https://w3id.org/credentials#",
"dc": "http://purl.org/dc/terms/",
"identity": "https://w3id.org/identity#",
"perm": "https://w3id.org/permissions#",
"ps": "https://w3id.org/payswarm#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"sec": "https://w3id.org/security#",
"schema": "http://schema.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"Group": "https://www.w3.org/ns/activitystreams#Group",
"claim": {"@id": "cred:claim", "@type": "@id"},
"credential": {"@id": "cred:credential", "@type": "@id"},
"issued": {"@id": "cred:issued", "@type": "xsd:dateTime"},
"issuer": {"@id": "cred:issuer", "@type": "@id"},
"recipient": {"@id": "cred:recipient", "@type": "@id"},
"Credential": "cred:Credential",
"CryptographicKeyCredential": "cred:CryptographicKeyCredential",
"about": {"@id": "schema:about", "@type": "@id"},
"address": {"@id": "schema:address", "@type": "@id"},
"addressCountry": "schema:addressCountry",
"addressLocality": "schema:addressLocality",
"addressRegion": "schema:addressRegion",
"comment": "rdfs:comment",
"created": {"@id": "dc:created", "@type": "xsd:dateTime"},
"creator": {"@id": "dc:creator", "@type": "@id"},
"description": "schema:description",
"email": "schema:email",
"familyName": "schema:familyName",
"givenName": "schema:givenName",
"image": {"@id": "schema:image", "@type": "@id"},
"label": "rdfs:label",
"name": "schema:name",
"postalCode": "schema:postalCode",
"streetAddress": "schema:streetAddress",
"title": "dc:title",
"url": {"@id": "schema:url", "@type": "@id"},
"Person": "schema:Person",
"PostalAddress": "schema:PostalAddress",
"Organization": "schema:Organization",
"identityService": {
"@id": "identity:identityService", "@type": "@id"
},
"idp": {"@id": "identity:idp", "@type": "@id"},
"Identity": "identity:Identity",
"paymentProcessor": "ps:processor",
"preferences": {"@id": "ps:preferences", "@type": "@vocab"},
"cipherAlgorithm": "sec:cipherAlgorithm",
"cipherData": "sec:cipherData",
"cipherKey": "sec:cipherKey",
"digestAlgorithm": "sec:digestAlgorithm",
"digestValue": "sec:digestValue",
"domain": "sec:domain",
"expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
"initializationVector": "sec:initializationVector",
"member": {"@id": "schema:member", "@type": "@id"},
"memberOf": {"@id": "schema:memberOf", "@type": "@id"},
"nonce": "sec:nonce",
"normalizationAlgorithm": "sec:normalizationAlgorithm",
"owner": {"@id": "sec:owner", "@type": "@id"},
"password": "sec:password",
"privateKey": {"@id": "sec:privateKey", "@type": "@id"},
"privateKeyPem": "sec:privateKeyPem",
"publicKey": {"@id": "sec:publicKey", "@type": "@id"},
"publicKeyPem": "sec:publicKeyPem",
"publicKeyService": {
"@id": "sec:publicKeyService", "@type": "@id"
},
"revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
"signature": "sec:signature",
"signatureAlgorithm": "sec:signatureAlgorithm",
"signatureValue": "sec:signatureValue",
"CryptographicKey": "sec:Key",
"EncryptedMessage": "sec:EncryptedMessage",
"GraphSignature2012": "sec:GraphSignature2012",
"LinkedDataSignature2015": "sec:LinkedDataSignature2015",
"accessControl": {"@id": "perm:accessControl", "@type": "@id"},
"writePermission": {"@id": "perm:writePermission", "@type": "@id"}
}
}
def getActivitystreamsSchema() -> {}:
# https://www.w3.org/ns/activitystreams
return {
"@context": {
"@vocab": "_:",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"as": "https://www.w3.org/ns/activitystreams#",
"ldp": "http://www.w3.org/ns/ldp#",
"vcard": "http://www.w3.org/2006/vcard/ns#",
"id": "@id",
"type": "@type",
"Accept": "as:Accept",
"Activity": "as:Activity",
"IntransitiveActivity": "as:IntransitiveActivity",
"Add": "as:Add",
"Announce": "as:Announce",
"Application": "as:Application",
"Arrive": "as:Arrive",
"Article": "as:Article",
"Audio": "as:Audio",
"Block": "as:Block",
"Collection": "as:Collection",
"CollectionPage": "as:CollectionPage",
"Relationship": "as:Relationship",
"Create": "as:Create",
"Delete": "as:Delete",
"Dislike": "as:Dislike",
"Document": "as:Document",
"Event": "as:Event",
"Follow": "as:Follow",
"Flag": "as:Flag",
"Group": "as:Group",
"Ignore": "as:Ignore",
"Image": "as:Image",
"Invite": "as:Invite",
"Join": "as:Join",
"Leave": "as:Leave",
"Like": "as:Like",
"Link": "as:Link",
"Mention": "as:Mention",
"Note": "as:Note",
"Object": "as:Object",
"Offer": "as:Offer",
"OrderedCollection": "as:OrderedCollection",
"OrderedCollectionPage": "as:OrderedCollectionPage",
"Organization": "as:Organization",
"Page": "as:Page",
"Person": "as:Person",
"Place": "as:Place",
"Profile": "as:Profile",
"Question": "as:Question",
"Reject": "as:Reject",
"Remove": "as:Remove",
"Service": "as:Service",
"TentativeAccept": "as:TentativeAccept",
"TentativeReject": "as:TentativeReject",
"Tombstone": "as:Tombstone",
"Undo": "as:Undo",
"Update": "as:Update",
"Video": "as:Video",
"View": "as:View",
"Listen": "as:Listen",
"Read": "as:Read",
"Move": "as:Move",
"Travel": "as:Travel",
"IsFollowing": "as:IsFollowing",
"IsFollowedBy": "as:IsFollowedBy",
"IsContact": "as:IsContact",
"IsMember": "as:IsMember",
"subject": {
"@id": "as:subject",
"@type": "@id"
},
"relationship": {
"@id": "as:relationship",
"@type": "@id"
},
"actor": {
"@id": "as:actor",
"@type": "@id"
},
"attributedTo": {
"@id": "as:attributedTo",
"@type": "@id"
},
"attachment": {
"@id": "as:attachment",
"@type": "@id"
},
"bcc": {
"@id": "as:bcc",
"@type": "@id"
},
"bto": {
"@id": "as:bto",
"@type": "@id"
},
"cc": {
"@id": "as:cc",
"@type": "@id"
},
"context": {
"@id": "as:context",
"@type": "@id"
},
"current": {
"@id": "as:current",
"@type": "@id"
},
"first": {
"@id": "as:first",
"@type": "@id"
},
"generator": {
"@id": "as:generator",
"@type": "@id"
},
"icon": {
"@id": "as:icon",
"@type": "@id"
},
"image": {
"@id": "as:image",
"@type": "@id"
},
"inReplyTo": {
"@id": "as:inReplyTo",
"@type": "@id"
},
"items": {
"@id": "as:items",
"@type": "@id"
},
"instrument": {
"@id": "as:instrument",
"@type": "@id"
},
"orderedItems": {
"@id": "as:items",
"@type": "@id",
"@container": "@list"
},
"last": {
"@id": "as:last",
"@type": "@id"
},
"location": {
"@id": "as:location",
"@type": "@id"
},
"next": {
"@id": "as:next",
"@type": "@id"
},
"object": {
"@id": "as:object",
"@type": "@id"
},
"oneOf": {
"@id": "as:oneOf",
"@type": "@id"
},
"anyOf": {
"@id": "as:anyOf",
"@type": "@id"
},
"closed": {
"@id": "as:closed",
"@type": "xsd:dateTime"
},
"origin": {
"@id": "as:origin",
"@type": "@id"
},
"accuracy": {
"@id": "as:accuracy",
"@type": "xsd:float"
},
"prev": {
"@id": "as:prev",
"@type": "@id"
},
"preview": {
"@id": "as:preview",
"@type": "@id"
},
"replies": {
"@id": "as:replies",
"@type": "@id"
},
"result": {
"@id": "as:result",
"@type": "@id"
},
"audience": {
"@id": "as:audience",
"@type": "@id"
},
"partOf": {
"@id": "as:partOf",
"@type": "@id"
},
"tag": {
"@id": "as:tag",
"@type": "@id"
},
"target": {
"@id": "as:target",
"@type": "@id"
},
"to": {
"@id": "as:to",
"@type": "@id"
},
"url": {
"@id": "as:url",
"@type": "@id"
},
"altitude": {
"@id": "as:altitude",
"@type": "xsd:float"
},
"content": "as:content",
"contentMap": {
"@id": "as:content",
"@container": "@language"
},
"name": "as:name",
"nameMap": {
"@id": "as:name",
"@container": "@language"
},
"duration": {
"@id": "as:duration",
"@type": "xsd:duration"
},
"endTime": {
"@id": "as:endTime",
"@type": "xsd:dateTime"
},
"height": {
"@id": "as:height",
"@type": "xsd:nonNegativeInteger"
},
"href": {
"@id": "as:href",
"@type": "@id"
},
"hreflang": "as:hreflang",
"latitude": {
"@id": "as:latitude",
"@type": "xsd:float"
},
"longitude": {
"@id": "as:longitude",
"@type": "xsd:float"
},
"mediaType": "as:mediaType",
"published": {
"@id": "as:published",
"@type": "xsd:dateTime"
},
"radius": {
"@id": "as:radius",
"@type": "xsd:float"
},
"rel": "as:rel",
"startIndex": {
"@id": "as:startIndex",
"@type": "xsd:nonNegativeInteger"
},
"startTime": {
"@id": "as:startTime",
"@type": "xsd:dateTime"
},
"summary": "as:summary",
"summaryMap": {
"@id": "as:summary",
"@container": "@language"
},
"totalItems": {
"@id": "as:totalItems",
"@type": "xsd:nonNegativeInteger"
},
"units": "as:units",
"updated": {
"@id": "as:updated",
"@type": "xsd:dateTime"
},
"width": {
"@id": "as:width",
"@type": "xsd:nonNegativeInteger"
},
"describes": {
"@id": "as:describes",
"@type": "@id"
},
"formerType": {
"@id": "as:formerType",
"@type": "@id"
},
"deleted": {
"@id": "as:deleted",
"@type": "xsd:dateTime"
},
"inbox": {
"@id": "ldp:inbox",
"@type": "@id"
},
"outbox": {
"@id": "as:outbox",
"@type": "@id"
},
"following": {
"@id": "as:following",
"@type": "@id"
},
"followers": {
"@id": "as:followers",
"@type": "@id"
},
"streams": {
"@id": "as:streams",
"@type": "@id"
},
"preferredUsername": "as:preferredUsername",
"endpoints": {
"@id": "as:endpoints",
"@type": "@id"
},
"uploadMedia": {
"@id": "as:uploadMedia",
"@type": "@id"
},
"proxyUrl": {
"@id": "as:proxyUrl",
"@type": "@id"
},
"liked": {
"@id": "as:liked",
"@type": "@id"
},
"oauthAuthorizationEndpoint": {
"@id": "as:oauthAuthorizationEndpoint",
"@type": "@id"
},
"oauthTokenEndpoint": {
"@id": "as:oauthTokenEndpoint",
"@type": "@id"
},
"provideClientKey": {
"@id": "as:provideClientKey",
"@type": "@id"
},
"signClientKey": {
"@id": "as:signClientKey",
"@type": "@id"
},
"sharedInbox": {
"@id": "as:sharedInbox",
"@type": "@id"
},
"Public": {
"@id": "as:Public",
"@type": "@id"
},
"source": "as:source",
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
}
}
}
def load_document(url):
"""
Retrieves JSON-LD at the given URL.
@ -367,49 +833,30 @@ def load_document(url):
# validate URL
pieces = urllib_parse.urlparse(url)
if (not all([pieces.scheme, pieces.netloc]) or
pieces.scheme not in ['http', 'https'] or
pieces.scheme not in ['http', 'https', 'dat'] or
set(pieces.netloc) > set(
string.ascii_letters + string.digits + '-.:')):
raise JsonLdError(
'URL could not be dereferenced; only "http" and "https" '
'URL could not be dereferenced; only http/https/dat '
'URLs are supported.',
'jsonld.InvalidUrl', {'url': url},
code='loading document failed')
https_handler = VerifiedHTTPSHandler()
url_opener = urllib_build_opener(https_handler)
url_opener.addheaders = [
('Accept', 'application/ld+json, application/json'),
('Accept-Encoding', 'deflate')]
with closing(url_opener.open(url)) as handle:
if handle.info().get('Content-Encoding') == 'gzip':
buf = io.BytesIO(handle.read())
f = gzip.GzipFile(fileobj=buf, mode='rb')
data = f.read()
else:
data = handle.read()
if url == 'https://w3id.org/identity/v1':
doc = {
'contextUrl': None,
'documentUrl': url,
'document': data.decode('utf8')
'document': getV1Schema()
}
doc['documentUrl'] = handle.geturl()
headers = dict(handle.info())
content_type = headers.get('content-type')
link_header = headers.get('link')
if link_header and content_type != 'application/ld+json':
link_header = parse_link_header(link_header).get(
LINK_HEADER_REL)
# only 1 related link header permitted
if isinstance(link_header, list):
raise JsonLdError(
'URL could not be dereferenced, it has more than one '
'associated HTTP Link Header.',
'jsonld.LoadDocumentError',
{'url': url},
code='multiple context link headers')
if link_header:
doc['contextUrl'] = link_header['target']
return doc
return doc
elif url == 'https://www.w3.org/ns/activitystreams':
doc = {
'contextUrl': None,
'documentUrl': url,
'document': getActivitystreamsSchema()
}
return doc
return None
except JsonLdError as e:
raise e
except Exception as cause:

View File

@ -86,8 +86,8 @@ from content import replaceContentDuplicates
from content import removeTextFormatting
from content import removeHtmlTag
from theme import setCSSparam
from jsonldsig import testSignJsonld
from jsonldsig import jsonldVerify
from linked_data_sig import generateJsonSignature
from linked_data_sig import verifyJsonSignature
from newsdaemon import hashtagRuleTree
from newsdaemon import hashtagRuleResolve
from newswire import getNewswireTags
@ -1977,11 +1977,14 @@ def testRemoveTextFormatting():
def testJsonld():
print("testJsonld")
jldDocument = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://somesite.net/users/gerbil",
"description": "My json document",
"numberField": 83582,
"object": {
"content": "Some content"
"content": "valid content"
}
}
# privateKeyPem, publicKeyPem = generateRSAKey()
@ -2022,14 +2025,43 @@ def testJsonld():
'TwIDAQAB\n' \
'-----END PUBLIC KEY-----'
signedDocument = testSignJsonld(jldDocument, privateKeyPem)
signedDocument = jldDocument.copy()
generateJsonSignature(signedDocument, privateKeyPem)
assert(signedDocument)
assert(signedDocument.get('signature'))
assert(signedDocument['signature'].get('signatureValue'))
assert(signedDocument['signature'].get('type'))
assert(len(signedDocument['signature']['signatureValue']) > 50)
assert(signedDocument['signature']['type'] == 'RsaSignatureSuite2017')
assert(jsonldVerify(signedDocument, publicKeyPem))
# print(str(signedDocument['signature']))
assert(signedDocument['signature']['type'] == 'RsaSignature2017')
assert(verifyJsonSignature(signedDocument, publicKeyPem))
# alter the signed document
signedDocument['object']['content'] = 'forged content'
assert(not verifyJsonSignature(signedDocument, publicKeyPem))
jldDocument2 = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://somesite.net/users/gerbil",
"description": "Another json document",
"numberField": 13353,
"object": {
"content": "More content"
}
}
signedDocument2 = jldDocument2.copy()
generateJsonSignature(signedDocument2, privateKeyPem)
assert(signedDocument2)
assert(signedDocument2.get('signature'))
assert(signedDocument2['signature'].get('signatureValue'))
# changed signature on different document
if signedDocument['signature']['signatureValue'] == \
signedDocument2['signature']['signatureValue']:
print('json signature has not changed for different documents')
assert '.' not in str(signedDocument['signature']['signatureValue'])
assert len(str(signedDocument['signature']['signatureValue'])) > 340
assert(signedDocument['signature']['signatureValue'] !=
signedDocument2['signature']['signatureValue'])
def testSiteIsActive():
@ -2978,9 +3010,36 @@ def testFunctions():
'-Gsep=+120 -Tx11 epicyon.dot')
def testLinksWithinPost() -> None:
baseDir = os.getcwd()
nickname = 'test27636'
domain = 'rando.site'
port = 443
httpPrefix = 'https'
content = 'This is a test post with links.\n\n' + \
'ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/v4/\n\nhttps://freedombone.net'
postJsonObject = \
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
content,
False, False, False, True,
None, None, False, None)
assert postJsonObject['object']['content'] == \
'<p>This is a test post with links.<br><br>' + \
'<a href="ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/v4/" ' + \
'rel="nofollow noopener noreferrer" target="_blank">' + \
'<span class="invisible">ftp://</span>' + \
'<span class="ellipsis">' + \
'ftp.ncdc.noaa.gov/pub/data/ghcn/v4/</span>' + \
'</a><br><br><a href="https://freedombone.net" ' + \
'rel="nofollow noopener noreferrer" target="_blank">' + \
'<span class="invisible">https://</span>' + \
'<span class="ellipsis">freedombone.net</span></a></p>'
def runAllTests():
print('Running tests...')
testFunctions()
testLinksWithinPost()
testReplyToPublicPost()
testGetMentionedPeople()
testGuessHashtagCategory()

View File

@ -349,5 +349,6 @@
"Unfilter words": "الكلمات غير المصفاة",
"Show Accounts": "إظهار الحسابات",
"Peertube Instances": "مثيلات Peertube",
"Show video previews for the following Peertube sites.": "إظهار معاينات الفيديو لمواقع Peertube التالية."
"Show video previews for the following Peertube sites.": "إظهار معاينات الفيديو لمواقع Peertube التالية.",
"Follows you": "يتبعك"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Paraules sense filtre",
"Show Accounts": "Mostra comptes",
"Peertube Instances": "Instàncies de Peertube",
"Show video previews for the following Peertube sites.": "Mostra les previsualitzacions de vídeo dels següents llocs de Peertube."
"Show video previews for the following Peertube sites.": "Mostra les previsualitzacions de vídeo dels següents llocs de Peertube.",
"Follows you": "Et segueix"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Geiriau di-hid",
"Show Accounts": "Dangos Cyfrifon",
"Peertube Instances": "Camau Peertube",
"Show video previews for the following Peertube sites.": "Dangos rhagolygon fideo ar gyfer y safleoedd Peertube canlynol."
"Show video previews for the following Peertube sites.": "Dangos rhagolygon fideo ar gyfer y safleoedd Peertube canlynol.",
"Follows you": "Yn eich dilyn chi"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Wörter herausfiltern",
"Show Accounts": "Konten anzeigen",
"Peertube Instances": "Peertube-Instanzen",
"Show video previews for the following Peertube sites.": "Zeigen Sie eine Videovorschau für die folgenden Peertube-Websites an."
"Show video previews for the following Peertube sites.": "Zeigen Sie eine Videovorschau für die folgenden Peertube-Websites an.",
"Follows you": "Folgt dir"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Unfilter words",
"Show Accounts": "Show Accounts",
"Peertube Instances": "Peertube Instances",
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites."
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites.",
"Follows you": "Follows you"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Palabras sin filtrar",
"Show Accounts": "Mostrar cuentas",
"Peertube Instances": "Instancias de Peertube",
"Show video previews for the following Peertube sites.": "Muestre vistas previas de video para los siguientes sitios de Peertube."
"Show video previews for the following Peertube sites.": "Muestre vistas previas de video para los siguientes sitios de Peertube.",
"Follows you": "Te sigue"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Mots non filtrés",
"Show Accounts": "Afficher les comptes",
"Peertube Instances": "Instances Peertube",
"Show video previews for the following Peertube sites.": "Afficher des aperçus vidéo pour les sites Peertube suivants."
"Show video previews for the following Peertube sites.": "Afficher des aperçus vidéo pour les sites Peertube suivants.",
"Follows you": "Vous suit"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Focail neamhleithleacha",
"Show Accounts": "Taispeáin Cuntais",
"Peertube Instances": "Imeachtaí Peertube",
"Show video previews for the following Peertube sites.": "Taispeáin réamhamharcanna físe do na suíomhanna Peertube seo a leanas."
"Show video previews for the following Peertube sites.": "Taispeáin réamhamharcanna físe do na suíomhanna Peertube seo a leanas.",
"Follows you": "Leanann tú"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "अनफ़िल्टर शब्द",
"Show Accounts": "खाते दिखाएं",
"Peertube Instances": "Peertube उदाहरण",
"Show video previews for the following Peertube sites.": "निम्नलिखित Peertube साइटों के लिए वीडियो पूर्वावलोकन दिखाएं।"
"Show video previews for the following Peertube sites.": "निम्नलिखित Peertube साइटों के लिए वीडियो पूर्वावलोकन दिखाएं।",
"Follows you": "आपका पीछा करता है"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Parole non filtrate",
"Show Accounts": "Mostra account",
"Peertube Instances": "Istanze di Peertube",
"Show video previews for the following Peertube sites.": "Mostra le anteprime dei video per i seguenti siti Peertube."
"Show video previews for the following Peertube sites.": "Mostra le anteprime dei video per i seguenti siti Peertube.",
"Follows you": "Ti segue"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "単語のフィルタリングを解除する",
"Show Accounts": "アカウントを表示する",
"Peertube Instances": "Peertubeインスタンス",
"Show video previews for the following Peertube sites.": "次のPeertubeサイトのビデオプレビューを表示します。"
"Show video previews for the following Peertube sites.": "次のPeertubeサイトのビデオプレビューを表示します。",
"Follows you": "あなたについていきます"
}

View File

@ -345,5 +345,6 @@
"Unfilter words": "Unfilter words",
"Show Accounts": "Show Accounts",
"Peertube Instances": "Peertube Instances",
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites."
"Show video previews for the following Peertube sites.": "Show video previews for the following Peertube sites.",
"Follows you": "Follows you"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Palavras sem filtro",
"Show Accounts": "Mostrar contas",
"Peertube Instances": "Instâncias Peertube",
"Show video previews for the following Peertube sites.": "Mostrar visualizações de vídeo para os seguintes sites Peertube."
"Show video previews for the following Peertube sites.": "Mostrar visualizações de vídeo para os seguintes sites Peertube.",
"Follows you": "Segue você"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "Не фильтровать слова",
"Show Accounts": "Показать счета",
"Peertube Instances": "Экземпляры Peertube",
"Show video previews for the following Peertube sites.": "Показать превью видео для следующих сайтов Peertube."
"Show video previews for the following Peertube sites.": "Показать превью видео для следующих сайтов Peertube.",
"Follows you": "Следует за вами"
}

View File

@ -349,5 +349,6 @@
"Unfilter words": "未过滤字词",
"Show Accounts": "显示帐户",
"Peertube Instances": "Peertube实例",
"Show video previews for the following Peertube sites.": "显示以下Peertube网站的视频预览。"
"Show video previews for the following Peertube sites.": "显示以下Peertube网站的视频预览。",
"Follows you": "跟着你"
}

View File

@ -19,6 +19,16 @@ from calendar import monthrange
from followingCalendar import addPersonToCalendar
def getLockedAccount(actorJson: {}) -> bool:
"""Returns whether the given account requires follower approval
"""
if not actorJson.get('manuallyApprovesFollowers'):
return False
if actorJson['manuallyApprovesFollowers'] is True:
return True
return False
def hasUsersPath(pathStr: str) -> bool:
"""Whether there is a /users/ path (or equivalent) in the given string
"""
@ -328,14 +338,16 @@ def removeIdEnding(idStr: str) -> str:
def getProtocolPrefixes() -> []:
"""Returns a list of valid prefixes
"""
return ('https://', 'http://', 'dat://', 'i2p://', 'gnunet://',
return ('https://', 'http://', 'ftp://',
'dat://', 'i2p://', 'gnunet://',
'hyper://', 'gemini://', 'gopher://')
def getLinkPrefixes() -> []:
"""Returns a list of valid web link prefixes
"""
return ('https://', 'http://', 'dat://', 'i2p://', 'gnunet://',
return ('https://', 'http://', 'ftp://',
'dat://', 'i2p://', 'gnunet://',
'hyper://', 'gemini://', 'gopher://', 'briar:')

View File

@ -18,6 +18,7 @@ from utils import removeHtml
from utils import getDomainFromActor
from utils import getNicknameFromActor
from blocking import isBlocked
from follow import isFollowerOfPerson
from follow import isFollowingActor
from followingCalendar import receivingCalendarEvents
from webapp_utils import htmlHeaderWithExternalStyle
@ -45,7 +46,8 @@ def htmlPersonOptions(defaultTimeline: str,
PGPfingerprint: str,
emailAddress: str,
dormantMonths: int,
backToPath: str) -> str:
backToPath: str,
lockedAccount: bool) -> str:
"""Show options for a person: view/follow/block/report
"""
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
@ -61,6 +63,7 @@ def htmlPersonOptions(defaultTimeline: str,
blockStr = 'Block'
nickname = None
optionsNickname = None
followsYou = False
if originPathStr.startswith('/users/'):
nickname = originPathStr.split('/users/')[1]
if '/' in nickname:
@ -76,6 +79,10 @@ def htmlPersonOptions(defaultTimeline: str,
optionsNickname = getNicknameFromActor(optionsActor)
optionsDomainFull = getFullDomain(optionsDomain, optionsPort)
followsYou = \
isFollowerOfPerson(baseDir,
nickname, domain,
optionsNickname, optionsDomainFull)
if isBlocked(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
blockStr = 'Block'
@ -112,11 +119,16 @@ def htmlPersonOptions(defaultTimeline: str,
'" ' + getBrokenLinkSubstitute() + '/></a>\n'
handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
handleShown = handle
if lockedAccount:
handleShown += '🔒'
if dormant:
handleShown += ' 💤'
optionsStr += \
' <p class="optionsText">' + translate['Options for'] + \
' @' + handleShown + '</p>\n'
if followsYou:
optionsStr += \
' <p class="optionsText">' + translate['Follows you'] + '</p>\n'
if emailAddress:
optionsStr += \
'<p class="imText">' + translate['Email'] + \

View File

@ -8,6 +8,7 @@ __status__ = "Production"
import os
from pprint import pprint
from utils import getLockedAccount
from utils import hasUsersPath
from utils import getFullDomain
from utils import isDormant
@ -37,6 +38,7 @@ from tox import getToxAddress
from briar import getBriarAddress
from jami import getJamiAddress
from filters import isFiltered
from follow import isFollowerOfPerson
from webapp_frontscreen import htmlFrontScreen
from webapp_utils import scheduledPostsExist
from webapp_utils import getPersonAvatarUrl
@ -164,6 +166,17 @@ def htmlProfileAfterSearch(cssCache: {},
displayName = searchNickname
if profileJson.get('name'):
displayName = profileJson['name']
lockedAccount = getLockedAccount(profileJson)
if lockedAccount:
displayName += '🔒'
followsYou = \
isFollowerOfPerson(baseDir,
nickname, domain,
searchNickname,
searchDomainFull)
profileDescription = ''
if profileJson.get('summary'):
profileDescription = profileJson['summary']
@ -218,7 +231,7 @@ def htmlProfileAfterSearch(cssCache: {},
searchNickname,
searchDomainFull,
translate,
displayName,
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl)
@ -324,6 +337,7 @@ def _getProfileHeaderAfterSearch(baseDir: str,
searchDomainFull: str,
translate: {},
displayName: str,
followsYou: bool,
profileDescriptionShort: str,
avatarUrl: str, imageUrl: str) -> str:
"""The header of a searched for handle, containing background
@ -346,6 +360,8 @@ def _getProfileHeaderAfterSearch(baseDir: str,
htmlStr += ' <h1>' + displayName + '</h1>\n'
htmlStr += \
' <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
if followsYou:
htmlStr += ' <p><b>' + translate['Follows you'] + '</b></p>\n'
htmlStr += ' <p>' + profileDescriptionShort + '</p>\n'
htmlStr += ' </figcaption>\n'
htmlStr += ' </figure>\n\n'