forked from indymedia/epicyon
Avoid post conversions between json and string after digest is calculated
parent
6c6b436cb7
commit
896b56b3a0
|
@ -134,7 +134,7 @@ def readFollowList(filename: str):
|
||||||
return followlist
|
return followlist
|
||||||
|
|
||||||
class PubServer(BaseHTTPRequestHandler):
|
class PubServer(BaseHTTPRequestHandler):
|
||||||
protocol_version = 'HTTP/1.1'
|
protocol_version = 'HTTP/1.0'
|
||||||
|
|
||||||
def _login_headers(self,fileFormat: str,length: int) -> None:
|
def _login_headers(self,fileFormat: str,length: int) -> None:
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|
20
httpsig.py
20
httpsig.py
|
@ -20,7 +20,7 @@ from time import gmtime, strftime
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
def messageContentDigest(messageBodyJsonStr: str) -> str:
|
def messageContentDigest(messageBodyJsonStr: str) -> str:
|
||||||
return base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()).decode('utf-8')
|
return base64.b64encode(SHA256.new(messageBodyJsonStr.encode('utf-8')).digest()).decode('utf-8')
|
||||||
|
|
||||||
def signPostHeaders(dateStr: str,privateKeyPem: str, \
|
def signPostHeaders(dateStr: str,privateKeyPem: str, \
|
||||||
nickname: str, \
|
nickname: str, \
|
||||||
|
@ -28,7 +28,7 @@ def signPostHeaders(dateStr: str,privateKeyPem: str, \
|
||||||
toDomain: str,toPort: int, \
|
toDomain: str,toPort: int, \
|
||||||
path: str, \
|
path: str, \
|
||||||
httpPrefix: str, \
|
httpPrefix: str, \
|
||||||
messageBodyJson: {}) -> str:
|
messageBodyJsonStr: str) -> str:
|
||||||
"""Returns a raw signature string that can be plugged into a header and
|
"""Returns a raw signature string that can be plugged into a header and
|
||||||
used to verify the authenticity of an HTTP transmission.
|
used to verify the authenticity of an HTTP transmission.
|
||||||
"""
|
"""
|
||||||
|
@ -44,14 +44,13 @@ def signPostHeaders(dateStr: str,privateKeyPem: str, \
|
||||||
|
|
||||||
if not dateStr:
|
if not dateStr:
|
||||||
dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime())
|
dateStr=strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime())
|
||||||
keyID = httpPrefix+'://'+domain+'/users/'+nickname+'#main-key'
|
keyID=httpPrefix+'://'+domain+'/users/'+nickname+'#main-key'
|
||||||
if not messageBodyJson:
|
if not messageBodyJsonStr:
|
||||||
headers = {'(request-target)': f'post {path}','host': toDomain,'date': dateStr,'content-type': 'application/json'}
|
headers={'(request-target)': f'post {path}','host': toDomain,'date': dateStr,'content-type': 'application/json'}
|
||||||
else:
|
else:
|
||||||
messageBodyJsonStr=json.dumps(messageBodyJson)
|
|
||||||
bodyDigest=messageContentDigest(messageBodyJsonStr)
|
bodyDigest=messageContentDigest(messageBodyJsonStr)
|
||||||
headers = {'(request-target)': f'post {path}','host': toDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/activity+json'}
|
headers={'(request-target)': f'post {path}','host': toDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': 'application/activity+json'}
|
||||||
privateKeyPem = RSA.import_key(privateKeyPem)
|
privateKeyPem=RSA.import_key(privateKeyPem)
|
||||||
#headers.update({
|
#headers.update({
|
||||||
# '(request-target)': f'post {path}',
|
# '(request-target)': f'post {path}',
|
||||||
#})
|
#})
|
||||||
|
@ -84,7 +83,7 @@ def createSignedHeader(privateKeyPem: str,nickname: str, \
|
||||||
domain: str,port: int, \
|
domain: str,port: int, \
|
||||||
toDomain: str,toPort: int, \
|
toDomain: str,toPort: int, \
|
||||||
path: str,httpPrefix: str,withDigest: bool, \
|
path: str,httpPrefix: str,withDigest: bool, \
|
||||||
messageBodyJson: {}) -> {}:
|
messageBodyJsonStr: str) -> {}:
|
||||||
"""Note that the domain is the destination, not the sender
|
"""Note that the domain is the destination, not the sender
|
||||||
"""
|
"""
|
||||||
contentType='application/activity+json'
|
contentType='application/activity+json'
|
||||||
|
@ -103,7 +102,6 @@ def createSignedHeader(privateKeyPem: str,nickname: str, \
|
||||||
domain,port,toDomain,toPort, \
|
domain,port,toDomain,toPort, \
|
||||||
path,httpPrefix,None)
|
path,httpPrefix,None)
|
||||||
else:
|
else:
|
||||||
messageBodyJsonStr=json.dumps(messageBodyJson)
|
|
||||||
bodyDigest=messageContentDigest(messageBodyJsonStr)
|
bodyDigest=messageContentDigest(messageBodyJsonStr)
|
||||||
print('***************************Send (request-target): post '+path)
|
print('***************************Send (request-target): post '+path)
|
||||||
print('***************************Send host: '+headerDomain)
|
print('***************************Send host: '+headerDomain)
|
||||||
|
@ -116,7 +114,7 @@ def createSignedHeader(privateKeyPem: str,nickname: str, \
|
||||||
signPostHeaders(dateStr,privateKeyPem,nickname, \
|
signPostHeaders(dateStr,privateKeyPem,nickname, \
|
||||||
domain,port, \
|
domain,port, \
|
||||||
toDomain,toPort, \
|
toDomain,toPort, \
|
||||||
path,httpPrefix,messageBodyJson)
|
path,httpPrefix,messageBodyJsonStr)
|
||||||
headers['signature'] = signatureHeader
|
headers['signature'] = signatureHeader
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
41
posts.py
41
posts.py
|
@ -25,7 +25,7 @@ from pprint import pprint
|
||||||
from random import randint
|
from random import randint
|
||||||
from session import createSession
|
from session import createSession
|
||||||
from session import getJson
|
from session import getJson
|
||||||
from session import postJson
|
from session import postJsonString
|
||||||
from session import postImage
|
from session import postImage
|
||||||
from webfinger import webfingerHandle
|
from webfinger import webfingerHandle
|
||||||
from httpsig import createSignedHeader
|
from httpsig import createSignedHeader
|
||||||
|
@ -879,7 +879,7 @@ def createReportPost(baseDir: str,
|
||||||
True,None, None, subject)
|
True,None, None, subject)
|
||||||
return postJsonObject
|
return postJsonObject
|
||||||
|
|
||||||
def threadSendPost(session,postJsonObject: {},federationList: [],\
|
def threadSendPost(session,postJsonStr: str,federationList: [],\
|
||||||
inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [],
|
inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [],
|
||||||
debug :bool) -> None:
|
debug :bool) -> None:
|
||||||
"""Sends a post with exponential backoff
|
"""Sends a post with exponential backoff
|
||||||
|
@ -887,14 +887,15 @@ def threadSendPost(session,postJsonObject: {},federationList: [],\
|
||||||
tries=0
|
tries=0
|
||||||
backoffTime=60
|
backoffTime=60
|
||||||
for attempt in range(20):
|
for attempt in range(20):
|
||||||
postResult = postJson(session,postJsonObject,federationList, \
|
postResult = \
|
||||||
inboxUrl,signatureHeaderJson, \
|
postJsonString(session,postJsonStr,federationList, \
|
||||||
"inbox:write")
|
inboxUrl,signatureHeaderJson, \
|
||||||
|
"inbox:write")
|
||||||
if postResult:
|
if postResult:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: json post to '+inboxUrl+' succeeded')
|
print('DEBUG: json post to '+inboxUrl+' succeeded')
|
||||||
if postJsonObject.get('published'):
|
#if postJsonObject.get('published'):
|
||||||
postLog.append(postJsonObject['published']+' '+postResult+'\n')
|
# postLog.append(postJsonObject['published']+' '+postResult+'\n')
|
||||||
# keep the length of the log finite
|
# keep the length of the log finite
|
||||||
# Don't accumulate massive files on systems with limited resources
|
# Don't accumulate massive files on systems with limited resources
|
||||||
while len(postLog)>64:
|
while len(postLog)>64:
|
||||||
|
@ -982,19 +983,23 @@ def sendPost(projectVersion: str, \
|
||||||
return 7
|
return 7
|
||||||
#postPath='/'+inboxUrl.split('/')[-1]
|
#postPath='/'+inboxUrl.split('/')[-1]
|
||||||
postPath=inboxUrl.split(toDomain)[1]
|
postPath=inboxUrl.split(toDomain)[1]
|
||||||
|
|
||||||
# construct the http header
|
# convert json to string so that there are no
|
||||||
|
# subsequent conversions after creating message body digest
|
||||||
|
postJsonStr=json.dumps(postJsonObject)
|
||||||
|
|
||||||
|
# construct the http header, including the message body digest
|
||||||
signatureHeaderJson = \
|
signatureHeaderJson = \
|
||||||
createSignedHeader(privateKeyPem,nickname,domain,port, \
|
createSignedHeader(privateKeyPem,nickname,domain,port, \
|
||||||
toDomain,toPort, \
|
toDomain,toPort, \
|
||||||
postPath,httpPrefix,withDigest,postJsonObject)
|
postPath,httpPrefix,withDigest,postJsonStr)
|
||||||
|
|
||||||
# Keep the number of threads being used small
|
# Keep the number of threads being used small
|
||||||
while len(sendThreads)>10:
|
while len(sendThreads)>10:
|
||||||
sendThreads[0].kill()
|
sendThreads[0].kill()
|
||||||
sendThreads.pop(0)
|
sendThreads.pop(0)
|
||||||
thr = threadWithTrace(target=threadSendPost,args=(session, \
|
thr = threadWithTrace(target=threadSendPost,args=(session, \
|
||||||
postJsonObject.copy(), \
|
postJsonStr, \
|
||||||
federationList, \
|
federationList, \
|
||||||
inboxUrl,baseDir, \
|
inboxUrl,baseDir, \
|
||||||
signatureHeaderJson.copy(), \
|
signatureHeaderJson.copy(), \
|
||||||
|
@ -1097,7 +1102,7 @@ def sendPostViaServer(projectVersion: str, \
|
||||||
'Content-type': 'application/json', \
|
'Content-type': 'application/json', \
|
||||||
'Authorization': authHeader}
|
'Authorization': authHeader}
|
||||||
postResult = \
|
postResult = \
|
||||||
postJson(session,postJsonObject,[],inboxUrl,headers,"inbox:write")
|
postJsonString(session,json.dumps(postJsonObject),[],inboxUrl,headers,"inbox:write")
|
||||||
#if not postResult:
|
#if not postResult:
|
||||||
# if debug:
|
# if debug:
|
||||||
# print('DEBUG: POST failed for c2s to '+inboxUrl)
|
# print('DEBUG: POST failed for c2s to '+inboxUrl)
|
||||||
|
@ -1211,12 +1216,16 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
print('DEBUG: '+toDomain+' is not in '+inboxUrl)
|
print('DEBUG: '+toDomain+' is not in '+inboxUrl)
|
||||||
return 7
|
return 7
|
||||||
postPath='/'+inboxUrl.split('/')[-1]
|
postPath='/'+inboxUrl.split('/')[-1]
|
||||||
|
|
||||||
# construct the http header
|
# convert json to string so that there are no
|
||||||
|
# subsequent conversions after creating message body digest
|
||||||
|
postJsonStr=json.dumps(postJsonObject)
|
||||||
|
|
||||||
|
# construct the http header, including the message body digest
|
||||||
signatureHeaderJson = \
|
signatureHeaderJson = \
|
||||||
createSignedHeader(privateKeyPem,nickname,domain,port, \
|
createSignedHeader(privateKeyPem,nickname,domain,port, \
|
||||||
toDomain,toPort, \
|
toDomain,toPort, \
|
||||||
postPath,httpPrefix,withDigest,postJsonObject)
|
postPath,httpPrefix,withDigest,postJsonStr)
|
||||||
|
|
||||||
# Keep the number of threads being used small
|
# Keep the number of threads being used small
|
||||||
while len(sendThreads)>10:
|
while len(sendThreads)>10:
|
||||||
|
@ -1227,7 +1236,7 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
pprint(postJsonObject)
|
pprint(postJsonObject)
|
||||||
thr = threadWithTrace(target=threadSendPost, \
|
thr = threadWithTrace(target=threadSendPost, \
|
||||||
args=(session, \
|
args=(session, \
|
||||||
postJsonObject.copy(), \
|
postJsonStr, \
|
||||||
federationList, \
|
federationList, \
|
||||||
inboxUrl,baseDir, \
|
inboxUrl,baseDir, \
|
||||||
signatureHeaderJson.copy(), \
|
signatureHeaderJson.copy(), \
|
||||||
|
|
18
session.py
18
session.py
|
@ -65,6 +65,24 @@ def postJson(session,postJsonObject: {},federationList: [],inboxUrl: str,headers
|
||||||
postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers)
|
postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers)
|
||||||
return postResult.text
|
return postResult.text
|
||||||
|
|
||||||
|
def postJsonString(session,postJsonStr: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str:
|
||||||
|
"""Post a json message string to the inbox of another person
|
||||||
|
Supplying a capability, such as "inbox:write"
|
||||||
|
NOTE: Here we post a string rather than the original json so that
|
||||||
|
conversions between string and json format don't invalidate
|
||||||
|
the message body digest of http signatures
|
||||||
|
"""
|
||||||
|
|
||||||
|
# always allow capability requests
|
||||||
|
if not capability.startswith('cap'):
|
||||||
|
# check that we are posting to a permitted domain
|
||||||
|
if not urlPermitted(inboxUrl,federationList,capability):
|
||||||
|
print('postJson: '+inboxUrl+' not permitted')
|
||||||
|
return None
|
||||||
|
|
||||||
|
postResult = session.post(url = inboxUrl, data = postJsonStr, headers=headers)
|
||||||
|
return postResult.text
|
||||||
|
|
||||||
def postImage(session,attachImageFilename: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str:
|
def postImage(session,attachImageFilename: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str:
|
||||||
"""Post an image to the inbox of another person or outbox via c2s
|
"""Post an image to the inbox of another person or outbox via c2s
|
||||||
Supplying a capability, such as "inbox:write"
|
Supplying a capability, such as "inbox:write"
|
||||||
|
|
8
tests.py
8
tests.py
|
@ -18,6 +18,7 @@ from person import createPerson
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
from httpsig import signPostHeaders
|
from httpsig import signPostHeaders
|
||||||
from httpsig import verifyPostHeaders
|
from httpsig import verifyPostHeaders
|
||||||
|
from httpsig import messageContentDigest
|
||||||
from cache import storePersonInCache
|
from cache import storePersonInCache
|
||||||
from cache import getPersonFromCache
|
from cache import getPersonFromCache
|
||||||
from threads import threadWithTrace
|
from threads import threadWithTrace
|
||||||
|
@ -103,14 +104,13 @@ def testHttpsigBase(withDigest):
|
||||||
domain, port, \
|
domain, port, \
|
||||||
boxpath, httpPrefix, None)
|
boxpath, httpPrefix, None)
|
||||||
else:
|
else:
|
||||||
bodyDigest = \
|
bodyDigest = messageContentDigest(messageBodyJsonStr)
|
||||||
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()).decode('utf-8')
|
|
||||||
headers = {'host': headersDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': contentType}
|
headers = {'host': headersDomain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': contentType}
|
||||||
signatureHeader = \
|
signatureHeader = \
|
||||||
signPostHeaders(dateStr,privateKeyPem, nickname, \
|
signPostHeaders(dateStr,privateKeyPem, nickname, \
|
||||||
domain, port, \
|
domain, port, \
|
||||||
domain, port, \
|
domain, port, \
|
||||||
boxpath, httpPrefix, messageBodyJson)
|
boxpath, httpPrefix, messageBodyJsonStr)
|
||||||
|
|
||||||
headers['signature'] = signatureHeader
|
headers['signature'] = signatureHeader
|
||||||
assert verifyPostHeaders(httpPrefix,publicKeyPem,headers, \
|
assert verifyPostHeaders(httpPrefix,publicKeyPem,headers, \
|
||||||
|
@ -128,7 +128,7 @@ def testHttpsigBase(withDigest):
|
||||||
else:
|
else:
|
||||||
# correct domain but fake message
|
# correct domain but fake message
|
||||||
messageBodyJsonStr = '{"a key": "a value", "another key": "Fake GNUs", "yet another key": "More Fake GNUs"}'
|
messageBodyJsonStr = '{"a key": "a value", "another key": "Fake GNUs", "yet another key": "More Fake GNUs"}'
|
||||||
bodyDigest = base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()).decode('utf-8')
|
bodyDigest = messageContentDigest(messageBodyJsonStr)
|
||||||
headers = {'host': domain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': contentType}
|
headers = {'host': domain,'date': dateStr,'digest': f'SHA-256={bodyDigest}','content-type': contentType}
|
||||||
headers['signature'] = signatureHeader
|
headers['signature'] = signatureHeader
|
||||||
assert verifyPostHeaders(httpPrefix,publicKeyPem,headers, \
|
assert verifyPostHeaders(httpPrefix,publicKeyPem,headers, \
|
||||||
|
|
Loading…
Reference in New Issue