diff --git a/auth-backend/auth-backend.py b/auth-backend/auth-backend.py index 136fec3..61b1ded 100755 --- a/auth-backend/auth-backend.py +++ b/auth-backend/auth-backend.py @@ -3,217 +3,50 @@ __author__ = "Vishwajeet Mishra " # Purpose: Main application file -import json, os from flask import Flask, request, Response -from urllib.parse import unquote_plus, quote_plus, urlparse -from requests import post -import datetime -import config -import sys -import watchdog.events +from urllib.parse import unquote_plus, urlparse +import config, utils +import sys, logging import watchdog.observers -class Handler(watchdog.events.PatternMatchingEventHandler): - def __init__(self): - # Set the patterns for PatternMatchingEventHandler - watchdog.events.PatternMatchingEventHandler.__init__(self, patterns=['*.flv'], - ignore_directories=True, case_sensitive=False) +app = Flask(__name__) - def on_created(self, event): - src = event.src_path - - if "%252F" not in src: - return - - record_name = src.split("record/")[1] - target = config.RECORD_TAR_DIR - record_name_split = record_name.split("%252F") - - for segment in range(len(record_name_split) - 1): - target += "/" + record_name_split[segment] - - if not os.path.exists(target): - os.makedirs(target) - - recording_unique_id_split = record_name_split[-1].rsplit("-",1) - - if len(recording_unique_id_split) == 2: - target += '/' + recording_unique_id_split[0] - - if not os.path.exists(target): - os.makedirs(target) - - target += '/' + recording_unique_id_split[1] - - if not os.path.islink(target): - os.symlink(src, target) - - -app = Flask(__name__, template_folder='.') - -token_cache = {} - - -def is_string_safe(string, exceptions="/.@-"): - if (not string or type(string) != str): - return False - - if (len(string) == 0 or len(string) > config.MAX_SAFE_STRING_LEN): - return False - - for ch in string: - if ( - (ch >= "a" and ch <= "z") or - (ch >= "A" and ch <= "Z") or - (ch >= "0" and ch <= "9") - ): - continue - - if (exceptions.find(ch) == -1): - return False - - return True - - -def is_valid_token(token): - if (not is_string_safe(token)): - return False - - split = token.split("/") - - if (len(split) != 2): - return False - - issued_by = split[0] - # issued_to = split[1] - random_hex = split[1] - - if (issued_by != config.AUTH_SERVER_NAME): - return False - - if (len(random_hex) != config.TOKEN_LEN_HEX): - return False - - return True - - -def symlink(resource_id): - resource_id_split = resource_id.split('/') - - hls_src_dir = config.HLS_SRC_DIR - - src = hls_src_dir + '/' + quote_plus(resource_id) - - for segment in range(len(resource_id_split) - 1): - hls_src_dir += '/' + resource_id_split[segment] - - if not os.path.exists(hls_src_dir): - os.makedirs(hls_src_dir) - - hls_src_dir += '/' + resource_id_split[-1] - - if not os.path.islink(hls_src_dir): - os.symlink(src, hls_src_dir) - - -def auth(introspect_response, resource_id, call): - if (not isinstance(introspect_response, dict) or not introspect_response or 'request' not in introspect_response): - print("Request not found in body.", file=sys.stderr) - return False - - for r in introspect_response['request']: - if (not r['scopes']): - print("Scopes not found in body.", file=sys.stderr) - return False - - if (r['id'] != resource_id): - continue - - if (call == 'play'): - if ("read" not in r['scopes']): - print("Read scope is not assigned.", file=sys.stderr) - return False - - elif (call == 'publish'): - if ("write" not in r['scopes']): - print("Write Scope is not assigned.", file=sys.stderr) - return False - - split = resource_id.split("/") - - if (len(split) > 7): - print("Request id too long", file=sys.stderr) - return False - - else: - print("Invalid Call",file=sys.stderr) - return False - - return True - return False - - -def validation(resource_id, token, call): - if (not is_valid_token(token)): - print("Invalid Token", file=sys.stderr) - return Response(status=403) - - if (token in token_cache): - - token_expiry = datetime.datetime.strptime(token_cache[token]['expiry'], '%Y-%m-%dT%H:%M:%S.%fZ') - - if (token_expiry < datetime.datetime.now()): - token_cache.pop(token) - else: - if (auth(token_cache[token], resource_id, call)): - symlink(resource_id) - return Response(status=200) - return Response(status=403) - - body = {'token': token} - - response = post( - url=config.INTROSPECT_URL, - headers={"content-type": "application/json"}, - data=json.dumps(body), - cert=(config.RS_CERT_FILE, config.RS_KEY_FILE), - verify=False - ) - if (response.status_code != 200): - return Response(status=response.status_code) - token_cache[token] = response.json() - if (auth(response.json(), resource_id, call)): - symlink(resource_id) - return Response(status=200) - return Response(status=403) +# TODO: Use Managed Cache library @app.route('/api/on-hls-auth', methods=['GET']) def on_hls_auth() -> Response: """ - API to authenticate on_hls_subcription - :return: + API to authenticate on_hls_subcription: + input: + Request: + request.environ contains 'HTTP_URL' and 'HTTP_TOKEN' + 'HTTP_URL': '/rtmp+hls//index.m3u8' + 'HTTP_TOKEN': '' + return: Response: status_code(200,403) """ + if ('HTTP_URL' not in request.environ or 'HTTP_TOKEN' not in request.environ or len(request.environ['HTTP_TOKEN']) == 0): - print('Invalid Input', file=sys.stderr) + logging.info('Invalid Input', file=sys.stderr) return Response(status=403) uri = urlparse(request.environ['HTTP_URL']) path_split = uri.path.split('/') + # TODO: Add detailed comments if len(path_split) != 4: - print("Invalid ID", file=sys.stderr) + logging.info("Invalid ID", file=sys.stderr) return Response(status=403) resource_id = unquote_plus(path_split[2]) token = unquote_plus(request.environ['HTTP_TOKEN']) - call = 'play' + request_type = 'play' - return validation(resource_id, token, call) + return Response(status=200 if utils.validation(resource_id, token, request_type) else 400) @app.route("/api/on-live-auth", methods=['POST']) @@ -223,17 +56,24 @@ def on_live_auth() -> Response: :return: Response: status_code(200,403) """ + if (not request or 'token' not in request.form or 'name' not in request.form or 'call' not in request.form): + return Response(status=403) + token = request.form['token'] resource_id = unquote_plus(request.form['name']) - call = request.form['call'] + request_type = request.form['call'] - return validation(resource_id, token, call) + return Response(status=200 if utils.validation(resource_id, token, request_type) else 400) if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) src_path = config.RECORD_SRC_DIR - event_handler = Handler() + event_handler = utils.Handler() observer = watchdog.observers.Observer() - observer.schedule(event_handler, path=src_path, recursive=True) + observer.schedule(event_handler, path=src_path, recursive=False) observer.start() - app.run(threaded=True, port=3001, host='0.0.0.0') + app.run(threaded=True, port=3001, host='0.0.0.0', debug=True) + + # TODO: ADD WSGI + # http_server = WSGIServer(('0.0.0.0', 3001), app, keyfile = 'ssl/privkey.pem', certfile='ssl/fullchain.pem') diff --git a/auth-backend/config.py b/auth-backend/config.py index b5bcaf9..6285e63 100644 --- a/auth-backend/config.py +++ b/auth-backend/config.py @@ -2,12 +2,11 @@ MAX_SAFE_STRING_LEN = 512 AUTH_SERVER_NAME = os.environ["AUTH_SERVER"] -TOKEN_LEN = 16 TOKEN_LEN_HEX = 32 INTROSPECT_URL = "https://"+ AUTH_SERVER_NAME + "/auth/v1/token/introspect" -RS_CERT_FILE = "/root/auth-backend/resource-server.pem" +RS_CERT_FILE = "/root/auth-backend/resource-server.pem" RS_KEY_FILE = "/root/auth-backend/resource-server.key.pem" -RECORD_SRC_DIR = '/root/datasetu-video-server/nginx/record/' -RECORD_TAR_DIR = '/root/datasetu-video-server/nginx/record' -HLS_SRC_DIR = '/root/datasetu-video-server/nginx/storage/rtmp+hls' - +RECORD_SRC_DIR = '/root/datasetu-video-server/nginx/record' +RECORD_DEST_DIR = '/root/datasetu-video-server/nginx/record/' +HLS_SRC_DIR = '/root/datasetu-video-server/nginx/storage/rtmp+hls' +HLS_DEST_DIR = '/root/datasetu-video-server/nginx/storage/rtmp+hls/' diff --git a/auth-backend/utils.py b/auth-backend/utils.py new file mode 100644 index 0000000..4a96ca7 --- /dev/null +++ b/auth-backend/utils.py @@ -0,0 +1,173 @@ +import json, os, sys, logging +from requests import post +import datetime +import watchdog.events +from urllib.parse import unquote_plus, quote_plus +import config + + +class Handler(watchdog.events.PatternMatchingEventHandler): + def __init__(self): + # Set the patterns for PatternMatchingEventHandler + watchdog.events.PatternMatchingEventHandler.__init__(self, patterns=['*.flv'], + ignore_directories=True, case_sensitive=False) + + def on_created(self, event): + src = event.src_path + logging.info(src, file=sys.stderr) + record_name = src.split("/")[-1] + target = config.RECORD_DEST_DIR + record_name_split = unquote_plus(unquote_plus(record_name)).split('/') + + target += '/'.join(record_name_split[:-1]) + + if not os.path.exists(target): + os.makedirs(target) + + recording_unique_id_split = record_name_split[-1].rsplit("-", 1) + + if len(recording_unique_id_split) == 2: + target += '/' + recording_unique_id_split[0] + + if not os.path.exists(target): + os.makedirs(target) + + target += '/' + recording_unique_id_split[1] + + if not os.path.islink(target): + os.symlink(src, target) + +# TODO: Use a managed cache library +token_cache = {} + + +def is_string_safe(string, exceptions="/.@-"): + if (not string or type(string) != str): + return False + + if (len(string) == 0 or len(string) > config.MAX_SAFE_STRING_LEN): + return False + + for ch in string: + if ( + (ch >= "a" and ch <= "z") or + (ch >= "A" and ch <= "Z") or + (ch >= "0" and ch <= "9") + ): + continue + + if (exceptions.find(ch) == -1): + return False + + return True + + +def is_valid_token(token): + if (not is_string_safe(token)): + return False + + split = token.split("/") + + if (len(split) != 2): + return False + + issued_by = split[0] + random_hex = split[1] + + # TODO: Check issued by against array of trusted auth servers + if (issued_by != config.AUTH_SERVER_NAME): + return False + + if (len(random_hex) != config.TOKEN_LEN_HEX): + return False + + return True + + +def symlink(resource_id): + resource_id_split = resource_id.split('/') + + hls_src_dir = config.HLS_SRC_DIR + + src = hls_src_dir + '/' + quote_plus(resource_id) + + dest = config.HLS_DEST_DIR + '/'.join(resource_id_split[:-1]) + + if not os.path.exists(dest): + os.makedirs(dest) + + dest += '/' + resource_id_split[-1] + + if not os.path.islink(dest): + os.symlink(src, dest) + + +# TODO: we may need to include checks for additional policy variables +def auth(introspect_response, resource_id, request_type): + if (not isinstance(introspect_response, dict) or not introspect_response or 'request' not in introspect_response): + logging.info("Request not found in body.", file=sys.stderr) + return False + + for r in introspect_response['request']: + if (r['id'] != resource_id): + continue + + if (request_type == 'play'): + if ("read" not in r['scopes']): + logging.info("Read scope is not assigned.", file=sys.stderr) + return False + + elif (request_type == 'publish'): + if ("write" not in r['scopes']): + logging.info("Write Scope is not assigned.", file=sys.stderr) + return False + + split = resource_id.split("/") + + # TODO: Align with aggregated Id implementation in vermillion + if (len(split) > 7): + logging.info("Request id too long", file=sys.stderr) + return False + + else: + logging.info("Invalid Call", file=sys.stderr) + return False + + return True + return False + + +def validation(resource_id, token, request_type): + if (not is_valid_token(token)): + logging.info("Invalid Token", file=sys.stderr) + return False + + if (token in token_cache): + + token_expiry = datetime.datetime.strptime(token_cache[token]['expiry'], '%Y-%m-%dT%H:%M:%S.%fZ') + + if (token_expiry < datetime.datetime.now()): + token_cache.pop(token) + + else: + if (auth(token_cache[token], resource_id, request_type)): + symlink(resource_id) + return True + return False + + body = {'token': token} + + response = post( + url=config.INTROSPECT_URL, + headers={"content-type": "application/json"}, + data=json.dumps(body), + cert=(config.RS_CERT_FILE, config.RS_KEY_FILE), + verify=(False if config.AUTH_SERVER_NAME == 'auth.local' else True) + ) + if (response.status_code != 200): + return False + token_cache[token] = response.json() + if (auth(response.json(), resource_id, request_type)): + symlink(resource_id) + return True + return False diff --git a/images/auth-backend/Dockerfile b/images/auth-backend/Dockerfile index c63d6fe..41810b9 100644 --- a/images/auth-backend/Dockerfile +++ b/images/auth-backend/Dockerfile @@ -5,8 +5,8 @@ label maintainer="Poorna Chandra Tejasvi