Skip to content

Commit

Permalink
Moved discovery code to a new module, to be further developed
Browse files Browse the repository at this point in the history
  • Loading branch information
glpatcern committed Jun 24, 2021
1 parent 7fc6508 commit 933fb8c
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 94 deletions.
87 changes: 87 additions & 0 deletions src/core/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'''
discovery.py
Helper code for the WOPI discovery phase, as well as
for integrating the apps supported by the bridge functionality.
'''

import time
import configparser
import json
import http.client
import requests
from xml.etree import ElementTree as ET
import flask
from urllib.parse import quote_plus as url_quote_plus
from urllib.parse import unquote as url_unquote
import core.wopiutils as utils

# convenience references to global entities
st = None
srv = None
log = None


def registerapp(srv):
pass


def initappsregistry(srv):
'''Initializes the CERNBox Office-like Apps Registry'''
# TODO to be deprecated in favour of a /wopi/iop/registerapp endpoint
oos = srv.config.get('general', 'oosurl', fallback=None)
if oos:
# The supported Microsoft Office Online end-points
srv.endpoints['.docx'] = {}
srv.endpoints['.docx']['view'] = oos + '/wv/wordviewerframe.aspx?edit=0'
srv.endpoints['.docx']['edit'] = oos + '/we/wordeditorframe.aspx?edit=1'
srv.endpoints['.docx']['new'] = oos + '/we/wordeditorframe.aspx?new=1' # pylint: disable=bad-whitespace
srv.endpoints['.xlsx'] = {}
srv.endpoints['.xlsx']['view'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=0'
srv.endpoints['.xlsx']['edit'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=1'
srv.endpoints['.xlsx']['new'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=1&new=1' # pylint: disable=bad-whitespace
srv.endpoints['.pptx'] = {}
srv.endpoints['.pptx']['view'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=ReadingView'
srv.endpoints['.pptx']['edit'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=EditView'
srv.endpoints['.pptx']['new'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=EditView&New=1' # pylint: disable=bad-whitespace
log.info('msg="Microsoft Office Online endpoints successfully configured" OfficeURL="%s"' % srv.endpoints['.docx']['edit'])

code = srv.config.get('general', 'codeurl', fallback=None)
if code:
try:
discData = requests.get(url=(code + '/hosting/discovery'), verify=False).content
discXml = ET.fromstring(discData)
# extract urlsrc from first <app> node inside <net-zone>
urlsrc = discXml.find('net-zone/app')[0].attrib['urlsrc']

# The supported Collabora end-points: as Collabora supports most Office-like files (including MS Office), we include here
# only the subset defined in the `codeofficetypes` configuration option, defaulting to just the core ODF types
codetypes = srv.config.get('general', 'codeofficetypes', fallback='.odt .ods .odp').split()
for t in codetypes:
srv.endpoints[t] = {}
srv.endpoints[t]['view'] = urlsrc + 'permission=readonly'
srv.endpoints[t]['edit'] = urlsrc + 'permission=edit'
srv.endpoints[t]['new'] = urlsrc + 'permission=edit' # pylint: disable=bad-whitespace
log.info('msg="Collabora Online endpoints successfully configured" count="%d" CODEURL="%s"' %
(len(codetypes), srv.endpoints['.odt']['edit']))

except (IOError, ET.ParseError) as e:
log.warning('msg="Failed to initialize Collabora Online endpoints" error="%s"' % e)

# The WOPI Bridge end-point
bridge = srv.config.get('general', 'wopibridgeurl', fallback=None)
if not bridge:
# fallback to the same WOPI url but on default port 8000
bridge = urllib.parse.urlsplit(srv.wopiurl)
bridge = '%s://%s:8000/wopib' % (bridge.scheme, bridge.netloc[:bridge.netloc.find(':')+1])
# The bridge only supports CodiMD for now, therefore this is hardcoded:
# once we move to the Apps Registry microservice, we can make it dynamic
srv.endpoints['.md'] = {}
srv.endpoints['.md']['view'] = srv.endpoints['.md']['edit'] = bridge + '/open'
srv.endpoints['.zmd'] = {}
srv.endpoints['.zmd']['view'] = srv.endpoints['.zmd']['edit'] = bridge + '/open'
srv.endpoints['.txt'] = {}
srv.endpoints['.txt']['view'] = srv.endpoints['.txt']['edit'] = bridge + '/open'
srv.endpoints['.epd'] = {} # Etherpad, for testing
srv.endpoints['.epd']['view'] = srv.endpoints['.epd']['edit'] = bridge + '/open'
log.info('msg="WOPI Bridge endpoints successfully configured" BridgeURL="%s"' % bridge)
5 changes: 3 additions & 2 deletions src/core/readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
## WOPI server - core module

This module includes the core WOPI protocol implementation, the discovery logic, and the storage access interfaces.
This module includes the core WOPI protocol implementation, along with the discovery logic
in the `discovery.py` module and the interoperable lock APIs in the `ioplocks.py` module.

Three storage interfaces are provided:
To access the storage, three interfaces are provided:

* `xrootiface.py` to interface to an EOS storage via the xrootd protocol. Though the code is generic enough to enable support for any xrootd-based storage, it does include EOS-specific calls.

Expand Down
10 changes: 4 additions & 6 deletions src/core/wopi.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ def checkFileInfo(fileid):
# populate app-specific metadata
# the following properties are only used by MS Office Online
if fExt in ['.docx', '.xlsx', '.pptx']:
# TODO once the endpoints are managed by Reva, this metadata has to be provided in the initial /open call
filemd['HostViewUrl'] = '%s&%s' % (srv.ENDPOINTS[fExt]['view'], wopiSrc)
filemd['HostEditUrl'] = '%s&%s' % (srv.ENDPOINTS[fExt]['edit'], wopiSrc)
filemd['HostViewUrl'] = '%s&%s' % (srv.endpoints[fExt]['view'], wopiSrc)
filemd['HostEditUrl'] = '%s&%s' % (srv.endpoints[fExt]['edit'], wopiSrc)
# the following actions are broken in MS Office Online, therefore they are disabled
filemd['SupportsRename'] = filemd['UserCanRename'] = False
# the following is to enable the 'Edit in Word/Excel/PowerPoint' (desktop) action (probably broken)
Expand Down Expand Up @@ -313,10 +312,9 @@ def putRelative(fileid, reqheaders, acctok):
putrelmd['Name'] = os.path.basename(targetName)
putrelmd['Url'] = '%s?access_token=%s' % (url_unquote(utils.generateWopiSrc(inode)), newacctok)
fExt = os.path.splitext(targetName)[1]
if fExt in srv.ENDPOINTS:
# TODO once the endpoints are managed by Reva, this metadata has to be provided in the initial /open call
if fExt in srv.endpoints:
putrelmd['HostEditUrl'] = '%s&WOPISrc=%s&access_token=%s' % \
(srv.ENDPOINTS[fExt]['edit'], \
(srv.endpoints[fExt]['edit'], \
utils.generateWopiSrc(inode), newacctok)
#else we don't know the app to edit this file type, therefore we do not provide the info
log.debug('msg="PutRelative response" token="%s" metadata="%s"' % (newacctok[-20:], putrelmd))
Expand Down
126 changes: 40 additions & 86 deletions src/wopiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
print("Missing modules, please install dependencies with `pip3 install -f requirements.txt`")
raise
import core.wopiutils as utils
import core.ioplocks as ioplocks
import core.wopi
import core.ioplocks
import core.discovery

# the following constant is replaced on the fly when generating the docker image
WOPISERVERVERSION = 'git'
Expand Down Expand Up @@ -66,6 +67,7 @@ class Wopi:
}
log = utils.JsonLogger(app.logger)
openfiles = {}
endpoints = {}

@classmethod
def init(cls):
Expand Down Expand Up @@ -110,80 +112,15 @@ def init(cls):
cls.lockpath = ''
_ = cls.config.get('general', 'downloadurl') # make sure this is defined
# initialize the submodules
utils.srv = ioplocks.srv = core.wopi.srv = cls
utils.log = ioplocks.log = core.wopi.log = cls.log
utils.st = ioplocks.st = core.wopi.st = storage
utils.srv = core.ioplocks.srv = core.wopi.srv = core.discovery.srv = cls
utils.log = core.ioplocks.log = core.wopi.log = core.discovery.log = cls.log
utils.st = core.ioplocks.st = core.wopi.st = storage
except (configparser.NoOptionError, OSError) as e:
# any error we get here with the configuration is fatal
cls.log.fatal('msg="Failed to initialize the service, aborting" error="%s"' % e)
print("Failed to initialize the service: %s\n" % e, file=sys.stderr)
sys.exit(22)

@classmethod
def initappsregistry(cls):
'''Initializes the CERNBox Office-like Apps Registry'''
# TODO all this is supposed to be moved to the CERNBox Apps Registry microservice at some stage in the future
cls.ENDPOINTS = {}

oos = cls.config.get('general', 'oosurl', fallback=None)
if oos:
# The supported Microsoft Office Online end-points
cls.ENDPOINTS['.docx'] = {}
cls.ENDPOINTS['.docx']['view'] = oos + '/wv/wordviewerframe.aspx?edit=0'
cls.ENDPOINTS['.docx']['edit'] = oos + '/we/wordeditorframe.aspx?edit=1'
cls.ENDPOINTS['.docx']['new'] = oos + '/we/wordeditorframe.aspx?new=1' # pylint: disable=bad-whitespace
cls.ENDPOINTS['.xlsx'] = {}
cls.ENDPOINTS['.xlsx']['view'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=0'
cls.ENDPOINTS['.xlsx']['edit'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=1'
cls.ENDPOINTS['.xlsx']['new'] = oos + '/x/_layouts/xlviewerinternal.aspx?edit=1&new=1' # pylint: disable=bad-whitespace
cls.ENDPOINTS['.pptx'] = {}
cls.ENDPOINTS['.pptx']['view'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=ReadingView'
cls.ENDPOINTS['.pptx']['edit'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=EditView'
cls.ENDPOINTS['.pptx']['new'] = oos + '/p/PowerPointFrame.aspx?PowerPointView=EditView&New=1' # pylint: disable=bad-whitespace
cls.log.info('msg="Microsoft Office Online endpoints successfully configured" OfficeURL="%s"' % cls.ENDPOINTS['.docx']['edit'])

code = cls.config.get('general', 'codeurl', fallback=None)
if code:
try:
import requests
from xml.etree import ElementTree as ET
discData = requests.get(url=(code + '/hosting/discovery'), verify=False).content
discXml = ET.fromstring(discData)
# extract urlsrc from first <app> node inside <net-zone>
urlsrc = discXml.find('net-zone/app')[0].attrib['urlsrc']

# The supported Collabora end-points: as Collabora supports most Office-like files (including MS Office), we include here
# only the subset defined in the `codeofficetypes` configuration option, defaulting to just the core ODF types
codetypes = cls.config.get('general', 'codeofficetypes', fallback='.odt .ods .odp').split()
for t in codetypes:
cls.ENDPOINTS[t] = {}
cls.ENDPOINTS[t]['view'] = urlsrc + 'permission=readonly'
cls.ENDPOINTS[t]['edit'] = urlsrc + 'permission=edit'
cls.ENDPOINTS[t]['new'] = urlsrc + 'permission=edit' # pylint: disable=bad-whitespace
cls.log.info('msg="Collabora Online endpoints successfully configured" count="%d" CODEURL="%s"' %
(len(codetypes), cls.ENDPOINTS['.odt']['edit']))

except (IOError, ET.ParseError) as e:
cls.log.warning('msg="Failed to initialize Collabora Online endpoints" error="%s"' % e)

# The WOPI Bridge end-point
bridge = cls.config.get('general', 'wopibridgeurl', fallback=None)
if not bridge:
# fallback to the same WOPI url but on default port 8000
bridge = urllib.parse.urlsplit(cls.wopiurl)
bridge = '%s://%s:8000/wopib' % (bridge.scheme, bridge.netloc[:bridge.netloc.find(':')+1])
# The bridge only supports CodiMD for now, therefore this is hardcoded:
# once we move to the Apps Registry microservice, we can make it dynamic
cls.ENDPOINTS['.md'] = {}
cls.ENDPOINTS['.md']['view'] = cls.ENDPOINTS['.md']['edit'] = bridge + '/open'
cls.ENDPOINTS['.zmd'] = {}
cls.ENDPOINTS['.zmd']['view'] = cls.ENDPOINTS['.zmd']['edit'] = bridge + '/open'
cls.ENDPOINTS['.txt'] = {}
cls.ENDPOINTS['.txt']['view'] = cls.ENDPOINTS['.txt']['edit'] = bridge + '/open'
cls.ENDPOINTS['.epd'] = {} # Etherpad, for testing
cls.ENDPOINTS['.epd']['view'] = cls.ENDPOINTS['.epd']['edit'] = bridge + '/open'
cls.log.info('msg="WOPI Bridge endpoints successfully configured" BridgeURL="%s"' % bridge)


@classmethod
def refreshconfig(cls):
Expand Down Expand Up @@ -338,6 +275,36 @@ def iopGetOpenFiles():
return flask.Response(json.dumps(jlist), mimetype='application/json')


#
# WOPI discovery endpoints
#
@Wopi.app.route("/wopi/cbox/endpoints", methods=['GET'])
@Wopi.metrics.do_not_track()
def cboxEndPoints():
'''Returns the office apps end-points registered with this WOPI server. This is used by the EFSS
client to discover which Apps frontends can be used with this WOPI server.
Note that if the end-points are relocated and the corresponding configuration entry updated,
the WOPI server must be restarted.'''
# TODO this endpoint should be moved to the Apps Registry service in Reva
Wopi.log.info('msg="cboxEndPoints: returning all registered office apps end-points" client="%s" mimetypesCount="%d"' %
(flask.request.remote_addr, len(Wopi.endpoints)))
return flask.Response(json.dumps(Wopi.endpoints), mimetype='application/json')


@Wopi.app.route("/wopi/iop/registerapp", methods=['POST'])
@Wopi.metrics.do_not_track()
def iopRegisterApp():
'''Register a new WOPI app
Required headers:
- Authorization: a bearer shared secret to protect this call
Request arguments:
- appname: a human-readable string to identify the app
- appurl: the URL of the app engine: it is expected that the WOPI discovery info can be gathered
by loading appurl + '/hosting/discovery'
'''
pass


#
# The WOPI protocol implementation starts here
#
Expand Down Expand Up @@ -425,7 +392,7 @@ def cboxLock():
filename = req.args['filename']
userid = req.args['userid'] if 'userid' in req.args else '0:0'
endpoint = req.args['endpoint'] if 'endpoint' in req.args else 'default'
return ioplocks.lock(filename, userid, endpoint, req.method == 'GET')
return core.ioplocks.lock(filename, userid, endpoint, req.method == 'GET')


@Wopi.app.route("/wopi/cbox/unlock", methods=['POST'])
Expand All @@ -452,25 +419,12 @@ def cboxUnlock():
filename = req.args['filename']
userid = req.args['userid'] if 'userid' in req.args else '0:0'
endpoint = req.args['endpoint'] if 'endpoint' in req.args else 'default'
return ioplocks.unlock(filename, userid, endpoint)
return core.ioplocks.unlock(filename, userid, endpoint)


#
# deprecated endpoints to be moved to Reva
# deprecated
#
@Wopi.app.route("/wopi/cbox/endpoints", methods=['GET'])
@Wopi.metrics.do_not_track()
def cboxEndPoints():
'''Returns the office apps end-points registered with this WOPI server. This is used by the EFSS
client to discover which Apps frontends can be used with this WOPI server.
Note that if the end-points are relocated and the corresponding configuration entry updated,
the WOPI server must be restarted.'''
# TODO this endpoint should be moved to the Apps Registry service in Reva
Wopi.log.info('msg="cboxEndPoints: returning all registered office apps end-points" client="%s" mimetypesCount="%d"' %
(flask.request.remote_addr, len(Wopi.ENDPOINTS)))
return flask.Response(json.dumps(Wopi.ENDPOINTS), mimetype='application/json')


@Wopi.app.route("/wopi/cbox/download", methods=['GET'])
def cboxDownload():
'''Returns the file's content for a given valid access token. Used as a download URL,
Expand Down Expand Up @@ -506,5 +460,5 @@ def cboxDownload():
#
if __name__ == '__main__':
Wopi.init()
Wopi.initappsregistry()
core.discovery.initappsregistry(Wopi)
Wopi.run()

0 comments on commit 933fb8c

Please sign in to comment.