Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ def _validateappname(appname):
def _gendocid(wopisrc):
'''Generate a URL safe hash of the wopisrc to be used as document id by the app'''
dig = hmac.new(WB.hashsecret.encode(), msg=wopisrc.split('/')[-1].encode(), digestmod=hashlib.sha1).digest()
return urlsafe_b64encode(dig).decode()[:-1]
docid = urlsafe_b64encode(dig).decode()[:-1]
WB.log.debug(f'msg="Generating docid" wopisrc="{wopisrc}" docid="{docid}"')
return docid


# The Bridge endpoints start here
Expand Down Expand Up @@ -328,6 +330,13 @@ def applist():
return flask.Response(json.dumps(WB.openfiles), mimetype='application/json')


def validatedocid(wopisrc, docid):
'''Validate that the given docid matches the wopiSrc'''
expecteddocid = _gendocid(wopisrc)
WB.log.debug(f'msg="Validate docid" expected="{expecteddocid}" actual="{docid}" wopisrc="{wopisrc}"')
return expecteddocid == docid


#############################################################################################################

def _intersection(boolsdict):
Expand Down
18 changes: 14 additions & 4 deletions src/bridge/codimd.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# initialized by the main class or by the init method
appurl = None
appexturl = None
apikey = None
log = None
sslverify = None
disablezip = None
Expand All @@ -39,8 +40,10 @@ def init(_appurl, _appinturl, _apikey):
'''Initialize global vars from the environment'''
global appurl
global appexturl
global apikey
appexturl = _appurl
appurl = _appinturl
apikey = _apikey
try:
# CodiMD integrates Prometheus metrics, let's probe if they exist
res = requests.head(appurl + '/metrics/codimd', verify=sslverify, timeout=10)
Expand Down Expand Up @@ -75,7 +78,7 @@ def getredirecturl(viewmode, wopisrc, acctok, docid, filename, displayname, reva
# Cloud storage to CodiMD
##########################

def _unzipattachments(inputbuf):
def _unzipattachments(inputbuf, wopisrc, acctok):
'''Unzip the given input buffer uploading the content to CodiMD and return the contained .md file'''
mddoc = None
try:
Expand All @@ -102,7 +105,10 @@ def _unzipattachments(inputbuf):
mddoc = mddoc.replace(bytes(zipinfo.filename), bytes(fname))
# OK, let's upload
log.debug(f'msg="Pushing attachment" filename="{fname}"')
res = requests.post(appurl + '/uploadimage', params={'generateFilename': 'false'},
res = requests.post(appurl + '/uploadimage',
params={'generateFilename': 'false',
'WOPISrc': wopisrc,
'accessToken': acctok},
files={'image': (fname, inputzip.read(zipinfo))}, verify=sslverify, timeout=10)
if res.status_code != http.client.OK:
log.error('msg="Failed to push included file" filename="%s" httpcode="%d"' % (fname, res.status_code))
Expand Down Expand Up @@ -144,7 +150,7 @@ def loadfromstorage(filemd, wopisrc, acctok, docid):

# if it's a bundled file, unzip it and push the attachments in the appropriate folder
if wasbundle and mdfile:
mddoc = _unzipattachments(mdfile)
mddoc = _unzipattachments(mdfile, wopisrc, acctok)
else:
mddoc = mdfile
# if the file was created on Windows, convert \r\n to \n for CodiMD to correctly edit it
Expand All @@ -157,7 +163,8 @@ def loadfromstorage(filemd, wopisrc, acctok, docid):
res = requests.post(appurl + '/new', data=mddoc,
allow_redirects=False,
params={'mode': 'locked'},
headers={'Content-Type': 'text/markdown'},
headers={'Content-Type': 'text/markdown',
'Authorization': f'Bearer {apikey}'},
verify=sslverify,
timeout=10)
if res.status_code == http.client.REQUEST_ENTITY_TOO_LARGE:
Expand All @@ -174,6 +181,7 @@ def loadfromstorage(filemd, wopisrc, acctok, docid):
# reserve the given docid in CodiMD via a HEAD request
res = requests.head(appurl + '/' + docid,
allow_redirects=False,
headers={'Authorization': f'Bearer {apikey}'},
verify=sslverify,
timeout=10)
if res.status_code not in (http.client.OK, http.client.FOUND):
Expand All @@ -190,6 +198,7 @@ def loadfromstorage(filemd, wopisrc, acctok, docid):

# push the document to CodiMD with the update API
res = requests.put(appurl + '/api/notes/' + docid,
headers={'Authorization': f'Bearer {apikey}'},
json={'content': mddoc.decode()},
verify=sslverify,
timeout=10)
Expand All @@ -205,6 +214,7 @@ def loadfromstorage(filemd, wopisrc, acctok, docid):
raise AppFailure

log.info(f'msg="Pushed document to CodiMD" docid="{docid}" token="{acctok[-20:]}"')

except requests.exceptions.RequestException as e:
log.error(f'msg="Exception raised attempting to connect to CodiMD" exception="{e}"')
raise AppFailure from e
Expand Down
31 changes: 26 additions & 5 deletions src/wopiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def iopDownload():
Wopi.log.info('msg="Expired or malformed token" client="%s" requestedUrl="%s" error="%s" token="%s"' %
(flask.request.remote_addr, flask.request.base_url, e,
(flask.request.args['access_token'] if 'access_token' in flask.request.args else 'N/A')))
return 'Invalid access token', http.client.UNAUTHORIZED
return 'Missing or invalid access token', http.client.UNAUTHORIZED


@Wopi.app.route("/wopi/iop/list", methods=['GET'])
Expand Down Expand Up @@ -528,11 +528,32 @@ def wopiPutFile(fileid):
#
# Bridge functionality
#
@Wopi.app.route("/wopi/bridge/<docid>", methods=["POST"])
@Wopi.app.route("/wopi/bridge/<docid>", methods=["HEAD", "POST"])
@Wopi.metrics.do_not_track()
def bridgeSave(docid):
'''The WOPI bridge save endpoint'''
return bridge.appsave(docid)
def bridgeRoot(docid):
'''The WOPI bridge endpoint'''
if flask.request.method == "HEAD":
# this is used to validate the access token
try:
acctok = jwt.decode(flask.request.args['access_token'], Wopi.wopisecret, algorithms=['HS256'])
if acctok['exp'] < time.time():
raise jwt.exceptions.ExpiredSignatureError
if bridge.validatedocid(flask.request.args['WOPISrc'], docid):
Wopi.log.info('msg="Bridge: access token is valid" filename="%s" token="%s"' %
(acctok['filename'], flask.request.args['access_token'][-20:]))
return '', http.client.OK
else:
Wopi.log.info('msg="Bridge: invalid docid for token" docid="%s" filename="%s" token="%s"' %
(docid, acctok['filename'], flask.request.args['access_token'][-20:]))
return 'Invalid docID for access token', http.client.UNAUTHORIZED
except (jwt.exceptions.DecodeError, jwt.exceptions.ExpiredSignatureError, KeyError) as e:
Wopi.log.info('msg="Bridge: expired or malformed token" client="%s" requestedUrl="%s" error="%s" token="%s"' %
(flask.request.remote_addr, flask.request.base_url, e,
(flask.request.args['access_token'] if 'access_token' in flask.request.args else 'N/A')))
return 'Missing or invalid access token', http.client.UNAUTHORIZED
else:
# this is used via web hooks by the apps to save the document
return bridge.appsave(docid)


@Wopi.app.route("/wopi/bridge/list", methods=["GET"])
Expand Down
Loading