Browse Source

Add module to handle incoming requests from the Nextcloud server

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
pull/8708/head
Daniel Calviño Sánchez 3 years ago
parent
commit
7f0d3071dd
  1. 1
      recording/pyproject.toml
  2. 203
      recording/src/nextcloud/talk/recording/Server.py
  3. 7
      recording/src/nextcloud/talk/recording/__main__.py

1
recording/pyproject.toml

@ -6,6 +6,7 @@ classifiers = [
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
]
dependencies = [
"flask",
"pulsectl",
"pyvirtualdisplay>=2.0",
"selenium>=4.6.0",

203
recording/src/nextcloud/talk/recording/Server.py

@ -0,0 +1,203 @@
#
# @copyright Copyright (c) 2023, Daniel Calviño Sánchez (danxuliu@gmail.com)
#
# @license GNU AGPL version 3 or any later version
#
# 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 the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Module to handle incoming requests.
"""
import atexit
import json
import hashlib
import hmac
from threading import Lock, Thread
from flask import Flask, jsonify, request
from werkzeug.exceptions import BadRequest, Forbidden
from nextcloud.talk import recording
from .Config import config
from .Service import RECORDING_STATUS_AUDIO_AND_VIDEO, Service
app = Flask(__name__)
services = {}
servicesLock = Lock()
@app.route("/api/v1/welcome", methods=["GET"])
def welcome():
return jsonify(version=recording.__version__)
@app.route("/api/v1/room/<token>", methods=["POST"])
def handleBackendRequest(token):
backend, data = _validateRequest()
if 'type' not in data:
raise BadRequest()
if data['type'] == 'start':
return startRecording(backend, token, data)
if data['type'] == 'stop':
return stopRecording(backend, token, data)
def _validateRequest():
"""
Validates the current request.
:return: the backend that sent the request and the object representation of
the body.
"""
if 'Talk-Recording-Backend' not in request.headers:
app.logger.warning("Missing Talk-Recording-Backend header")
raise Forbidden()
backend = request.headers['Talk-Recording-Backend']
secret = config.getBackendSecret(backend)
if not secret:
app.logger.warning(f"No secret configured for backend {backend}")
raise Forbidden()
if 'Talk-Recording-Random' not in request.headers:
app.logger.warning("Missing Talk-Recording-Random header")
raise Forbidden()
random = request.headers['Talk-Recording-Random']
if 'Talk-Recording-Checksum' not in request.headers:
app.logger.warning("Missing Talk-Recording-Checksum header")
raise Forbidden()
checksum = request.headers['Talk-Recording-Checksum']
maximumMessageSize = config.getBackendMaximumMessageSize(backend)
if not request.content_length or request.content_length > maximumMessageSize:
app.logger.warning(f"Message size above limit: {request.content_length} {maximumMessageSize}")
raise BadRequest()
body = request.get_data()
expectedChecksum = _calculateChecksum(secret, random, body)
if not hmac.compare_digest(checksum, expectedChecksum):
app.logger.warning(f"Checksum verification failed: {checksum} {expectedChecksum}")
raise Forbidden()
return backend, json.loads(body)
def _calculateChecksum(secret, random, body):
secret = secret.encode()
message = random.encode() + body
hmacValue = hmac.new(secret, message, hashlib.sha256)
return hmacValue.hexdigest()
def startRecording(backend, token, data):
serviceId = f'{backend}-{token}'
if 'start' not in data:
raise BadRequest()
if 'owner' not in data['start']:
raise BadRequest()
status = RECORDING_STATUS_AUDIO_AND_VIDEO
if 'status' in data['start']:
status = data['start']['status']
owner = data['start']['owner']
service = None
with servicesLock:
if serviceId in services:
app.logger.warning(f"Trying to start recording again: {backend} {token}")
return {}
service = Service(backend, token, status, owner)
services[serviceId] = service
app.logger.info(f"Start recording: {backend} {token}")
serviceStartThread = Thread(target=_startRecordingService, args=[service], daemon=True)
serviceStartThread.start()
return {}
def _startRecordingService(service):
"""
Helper function to start a recording service.
The recording service will be removed from the list of services if it can
not be started.
:param service: the Service to start.
"""
serviceId = f'{service.backend}-{service.token}'
try:
service.start()
except Exception as exception:
with servicesLock:
if serviceId not in services:
# Service was already stopped, exception should have been caused
# by stopping the helpers even before the recorder started.
app.logger.info(f"Recording stopped before starting: {service.backend} {service.token}", exc_info=exception)
return
app.logger.exception(f"Failed to start recording: {service.backend} {service.token}")
services.pop(serviceId)
def stopRecording(backend, token, data):
serviceId = f'{backend}-{token}'
service = None
with servicesLock:
if serviceId not in services:
app.logger.warning(f"Trying to stop unknown recording: {backend} {token}")
return {}
service = services[serviceId]
services.pop(serviceId)
app.logger.info(f"Stop recording: {backend} {token}")
serviceStopThread = Thread(target=service.stop, daemon=True)
serviceStopThread.start()
return {}
# Despite this handler it seems that in some cases the geckodriver could have
# been killed already when it is executed, which unfortunately prevents a proper
# cleanup of the temporary files opened by the browser.
def _stopServicesOnExit():
with servicesLock:
serviceIds = list(services.keys())
for serviceId in serviceIds:
service = services.pop(serviceId)
del service
# Services should be explicitly deleted before exiting, as if they are
# implicitly deleted while exiting the Selenium driver may not cleanly quit.
atexit.register(_stopServicesOnExit)

7
recording/src/nextcloud/talk/recording/__main__.py

@ -21,6 +21,7 @@ import argparse
import logging
from .Config import config
from .Server import app
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--config", help="path to configuration file", default="server.conf")
@ -29,3 +30,9 @@ args = parser.parse_args()
config.load(args.config)
logging.basicConfig(level=config.getLogLevel())
logging.getLogger('werkzeug').setLevel(config.getLogLevel())
listen = config.getListen()
host, port = listen.split(':')
app.run(host, port, threaded=True)
Loading…
Cancel
Save