Browse Source
Add module to handle incoming requests from the Nextcloud server
Add module to handle incoming requests from the Nextcloud server
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>pull/8708/head
3 changed files with 211 additions and 0 deletions
-
1recording/pyproject.toml
-
203recording/src/nextcloud/talk/recording/Server.py
-
7recording/src/nextcloud/talk/recording/__main__.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) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue