moved everything to a new module called backend

This commit is contained in:
2019-10-23 15:00:33 +02:00
parent 310d5f4820
commit 6b4f7c8118
52 changed files with 2 additions and 380 deletions

70
backend/__init__.py Normal file
View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
"""
Backend base module
"""
import jwt
import requests
from flask import Flask, jsonify
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
from flask_jwt_extended import JWTManager, decode_token
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
app = Flask(__name__)
app.config.from_object('backend.config.Config')
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
# flask_jwt_extended: to be used usually by API
jwt_extended = JWTManager(app)
#
jwt_auth = HTTPTokenAuth('Bearer')
@jwt_auth.verify_token
def verify_token(token):
"""This function (and HTTPTokenAuth('Bearer')) has been defined to be used together with MultiAuth. For API calls
solely using JWT authentication, jwt_required of flask_jwt_extended should be used directly."""
app.logger.info(token)
try:
decoded = decode_token(token)
except jwt.exceptions.DecodeError as e:
app.logger.warn("Could not verify token: {}".format(str(e)))
return False
except jwt.exceptions.ExpiredSignatureError as e:
app.logger.warn("Could not verify token: {}".format(str(e)))
return False
app.logger.info(decoded)
return True
basic_auth = HTTPBasicAuth()
multi_auth = MultiAuth(basic_auth, jwt_auth)
from backend.auth import oidc_auth, auth_bp
try:
oidc_auth.init_app(app)
except requests.exceptions.ConnectionError as err:
app.logger.error("Could not connect to OIDC!!", err)
# oidc_multi_auth = MultiAuth(oidc_auth, jwt_auth) <- can't work as OIDCAuthentication not implementing HTTPAuth
from .serve_frontend import fe_bp
from .api import auth_api_bp, api_v1, api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(auth_api_bp)
app.register_blueprint(api_bp)
app.register_blueprint(fe_bp)
CORS(app)
CORS(api_bp)
# Fix flask-restplus by duck typing error handlers
jwt_extended._set_error_handler_callbacks(api_v1)

41
backend/__main__.py Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2019. Tobias Kurze
import logging
import ssl
from jinja2.exceptions import TemplateNotFound
from backend import app, db
from backend.models import pre_fill_table
from backend.tools.model_updater import update_recorder_models_database
def main():
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
print(app.config.get("SERVER_NAME", None))
server_name = app.config.get("SERVER_NAME", None)
if server_name is not None and "ubkaps154.ubka.uni-karlsruhe.de" in server_name:
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain('cert.pem', 'key.pem')
app.run(debug=True, ssl_context=context, threaded=True)
except FileNotFoundError:
app.run(debug=True, threaded=True)
try:
db.create_all()
except Exception as e:
logging.critical(e)
pre_fill_table()
update_recorder_models_database()
app.run(debug=True, host="0.0.0.0")
if __name__ == '__main__':
main()

78
backend/api/__init__.py Normal file
View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, abort
from flask_restplus import Api, Namespace
api_authorizations = {
'apikey': {
'type': 'apiKey',
'in': 'header',
'name': 'X-API-KEY'
},
'basicAuth': {
'type': 'basic',
'scheme': 'basic'
},
'bearerAuth': {
'type': 'apiKey',
'scheme': 'bearer',
'name': 'Authorization',
'in': 'header'
}
}
api_bp = Blueprint('api', __name__, url_prefix='/api')
api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API',
description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
api_user = Namespace('user', description="User management namespace", authorizations=api_authorizations)
api_group = Namespace('group', description="Group management namespace", authorizations=api_authorizations)
api_room = Namespace('room', description="Room management namespace", authorizations=api_authorizations)
api_recorder = Namespace('recorder', description="Recorder management namespace", authorizations=api_authorizations)
api_virtual_command = Namespace('virtual_command', description="Virtual command namespace",
authorizations=api_authorizations)
api_cron_job = Namespace('cron_job', description="Cron job namespace",
authorizations=api_authorizations)
api_control = Namespace('control', description="Control namespace",
authorizations=api_authorizations)
api_v1.add_namespace(api_user)
api_v1.add_namespace(api_group)
api_v1.add_namespace(api_room)
api_v1.add_namespace(api_recorder)
api_v1.add_namespace(api_virtual_command)
api_v1.add_namespace(api_cron_job)
api_v1.add_namespace(api_control)
auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth')
auth_api_v1 = Api(auth_api_bp, prefix="/v1", version='0.1', title='Auth API',
description='Auth API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
auth_api_providers_ns = Namespace('providers')
auth_api_register_ns = Namespace('register')
auth_api_v1.add_namespace(auth_api_providers_ns)
auth_api_v1.add_namespace(auth_api_register_ns)
# user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user')
# group_api_bp = Blueprint('group_api', __name__, url_prefix='/api/group')
from .example_api import *
from .auth_api import *
from .user_api import *
from .group_api import *
from .room_api import *
from .recorder_api import *
from .control_api import *
from .virtual_command_api import *
# from .group_api import *
@api_bp.route('/<path:path>')
def catch_all_api(path):
"""
Default 404 response for undefined paths in API.
:param path:
:return:
"""
abort(404)

196
backend/api/auth_api.py Normal file
View File

@@ -0,0 +1,196 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
import base64
import json
from pprint import pprint
import flask
from datetime import datetime, timedelta
import jwt
from flask import request, jsonify, current_app, url_for, Response, session, redirect, make_response
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_refresh_token_required, get_jwt_identity, \
get_raw_jwt, jwt_required
from functools import wraps
from random import randint
from flask_login import logout_user, login_user
from typing import Iterable
from flask_restplus import Resource, fields
from werkzeug.routing import BuildError
from backend import db, app, jwt_extended
from backend.api import auth_api_bp, auth_api_providers_ns, auth_api_register_ns
from backend.auth import AUTH_PROVIDERS, oidc_auth
from backend.models.user_model import User, Group, BlacklistToken
@auth_api_bp.route('/providers', methods=('GET',))
def get_auth_providers():
providers = dict()
for p in AUTH_PROVIDERS:
provider = dict(AUTH_PROVIDERS[p])
try:
provider["url"] = url_for(AUTH_PROVIDERS[p]["url"])
except BuildError:
provider["url"] = AUTH_PROVIDERS[p]["url"]
providers[p] = provider
return jsonify(providers)
@auth_api_providers_ns.route('/')
class AuthProviders(Resource):
def get(self):
return get_auth_providers()
@auth_api_bp.route('/register', methods=('POST',))
def register():
data = request.get_json()
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
@auth_api_register_ns.route('/')
@auth_api_register_ns.expect(auth_api_register_ns.model('RegisterModel', {
'nickname': fields.String(required=False, description='The user\'s nickname'),
'first_name': fields.String(required=False, description='The user\'s first name'),
'last_name': fields.String(required=False, description='The user\'s last name'),
'lang': fields.String(required=False, description='The user\'s preferred language'),
'timezone': fields.String(required=False, description='The user\'s preferred timezone'),
'email': fields.String(required=True, description='The user\'s e-mail address'),
'password': fields.String(required=False, description='The group\'s name')
}))
class AuthProviders(Resource):
def get(self):
return register()
@auth_api_bp.route('/login', methods=('GET', 'POST',))
def login():
print("login")
print(request)
data = request.get_json()
if not data:
return jsonify({'message': 'Invalid request data', 'authenticated': False}), 401
print(data)
user = User.authenticate(**data)
if not user:
return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
token = {
'access_token': create_access_token(identity=user, fresh=True),
'refresh_token': create_refresh_token(identity=user)
}
return jsonify(token), 200
# Endpoint for revoking the current users access token
@auth_api_bp.route('/logout', methods=['GET', 'DELETE'])
@jwt_required
def logout():
jti = get_raw_jwt()['jti']
db.session.add(BlacklistToken(token=jti))
db.session.commit()
return jsonify({"msg": "Successfully logged out"}), 200
# Endpoint for revoking the current users refresh token
@auth_api_bp.route('/logout2', methods=['GET', 'DELETE'])
@jwt_refresh_token_required
def logout2():
jti = get_raw_jwt()['jti']
db.session.add(BlacklistToken(token=jti))
db.session.commit()
return jsonify({"msg": "Successfully logged out"}), 200
def check_and_create_groups(groups: Iterable[str]):
user_groups = []
for g in groups:
group = Group.get_by_name(g)
if group is None:
group = Group(name=g)
db.session.add(group)
user_groups.append(group)
db.session.commit()
return user_groups
def create_or_retrieve_user_from_userinfo(userinfo):
try:
email = userinfo["email"]
except KeyError:
return None
user_groups = check_and_create_groups(groups=userinfo.get("memberOf", []))
user = User.get_by_identifier(email)
if user is not None:
app.logger.info("user found -> update user")
pprint(user.to_dict())
user.first_name = userinfo.get("given_name", "")
user.last_name = userinfo.get("family_name", "")
for g in user_groups:
user.groups.append(g)
db.session.commit()
return user
user = User(email=email, first_name=userinfo.get("given_name", ""),
last_name=userinfo.get("family_name", ""), external_user=True,
groups=user_groups)
app.logger.info("creating new user")
db.session.add(user)
db.session.commit()
return user
@auth_api_bp.route('/oidc', methods=['GET'])
@auth_api_bp.route('/oidc/<redirect_url>', methods=['GET'])
@oidc_auth.oidc_auth()
def oidc(redirect_url=None):
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
if user is None:
return "Could not authenticate: could not find or create user.", 401
if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False):
token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY'])
else:
token = json.dumps({
'access_token': create_access_token(identity=user, fresh=True),
'refresh_token': create_refresh_token(identity=user)
})
if redirect_url is None:
redirect_url = request.headers.get("Referer")
if redirect_url is None:
redirect_url = request.args.get('redirect_url')
if redirect_url is None:
redirect_url = "/"
app.logger.info("Token: {}".format(token))
response = make_response(redirect(redirect_url))
response.set_cookie('tokens', base64.b64encode(token.encode('utf-8')))
return response
@auth_api_bp.route('/refresh', methods=['GET'])
@jwt_refresh_token_required
def refresh():
"""Refresh token endpoint. This will generate a new access token from
the refresh token, but will mark that access token as non-fresh,
as we do not actually verify a password in this endpoint."""
jwt_identity = get_jwt_identity()
user = User.get_by_identifier(jwt_identity)
app.logger.info("Refreshing token for " + str(user))
new_token = create_access_token(identity=user, fresh=False)
ret = {'access_token': new_token}
return jsonify(ret), 200

View File

@@ -0,0 +1,42 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
import json
from datetime import datetime
from flask_jwt_extended import jwt_required, get_current_user, get_jwt_claims
from flask_restplus import fields, Resource
from backend import db
from backend.api import api_control, get_jwt_identity
control_command_response_model = api_control.model('Control Command Response', {
'time': fields.DateTime(required=False, description='Creation date of the recorder'),
'state': fields.String(min_length=3, required=True, description='The recorder\'s name'),
'output': fields.String(required=False, description='The recorder\'s description'),
'error': fields.String(required=False, description='The recorder\'s description'),
})
@api_control.route('')
class ControlCommand(Resource):
control_command_parser = api_control.parser()
control_command_parser.add_argument('recorder_id', type=int, default=1, required=True)
control_command_parser.add_argument('command_id', type=int, default=1, required=True)
control_command_parser.add_argument('parameters', default=json.dumps({'p1': 'v1'}), type=dict, required=False,
location='json')
@jwt_required
@api_control.doc('run_command')
@api_control.expect(control_command_parser)
@api_control.marshal_with(control_command_response_model, skip_none=False, code=201)
def post(self):
print(get_current_user())
print(get_jwt_identity())
current_user = {'user': get_current_user(), 'claims': get_jwt_claims()}
args = self.control_command_parser.parse_args()
return {'time': datetime.utcnow(), 'output': args, 'state': current_user}

134
backend/api/example_api.py Normal file
View File

@@ -0,0 +1,134 @@
import datetime
import ipaddress
import json
import logging
from random import *
from flask import jsonify, Blueprint, request
from flask_restplus import Resource, reqparse
from backend import basic_auth, multi_auth, db, jwt_auth
from backend.api import api_v1, api_bp
@api_bp.route('/random')
def random_number():
"""
:return: a random number
"""
response = {
'randomNumber': randint(1, 100)
}
return jsonify(response)
@api_bp.route('/test_jwt')
@jwt_auth.login_required
def random_number_jwt():
"""
:return: a random number
"""
response = {
'randomNumber': randint(1, 100)
}
return jsonify(response)
class HelloWorld(Resource):
"""
This is a test class.
"""
def get(self):
"""
just a test!
:return: Hello: World
"""
print(str(self))
return {'hello': 'world'}
api_v1.add_resource(HelloWorld, '/')
class SensorData_Handler(Resource):
parser = reqparse.RequestParser()
parser.add_argument('values', type=str, required=True, help="sensor data values are required (as JSON list)")
@basic_auth.login_required
def get(self, mac):
sensor = Sensor.get_by_mac_user(mac, g.user)
if not sensor:
return "sensor not found", 404
return jsonify(sensor.get_sensor_data())
# return {'task': 'id: ' + mac}
@multi_auth.login_required
def post(self, mac):
sensor = Sensor.get_by_mac(mac)
if not sensor:
return "sensor not found", 404
print("JSON")
print(request.json)
print(request.json['values'])
args = SensorData_Handler.parser.parse_args()
print("values...")
print(args['values'])
values = json.loads(args['values'])
app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
rough_geo_location = None
try:
rough_geo_location_info = None
ip = ipaddress.ip_address(request.remote_addr)
if request.remote_addr == "127.0.0.1":
ip = ipaddress.ip_address("89.245.37.108")
if isinstance(ip, ipaddress.IPv4Address):
rough_geo_location_info = geoip4.record_by_addr(str(ip))
elif isinstance(ip, ipaddress.IPv6Address):
rough_geo_location_info = geoip6.record_by_addr(str(ip))
if rough_geo_location_info is not None:
rough_geo_location = json.dumps({key: rough_geo_location_info[key] for key in ['country_code',
'country_name',
'postal_code',
'city',
'time_zone',
'latitude',
'longitude']
})
except ValueError:
pass
for values_at_time in values:
wasss_app.logger.info("values_at_time: " + str(values_at_time) + " (len: " + str(len(values_at_time)) + ")")
sensor_data = SensorData(sensor=sensor, rough_geo_location=rough_geo_location)
val_cycle = cycle(values_at_time)
for sensor_data_header in sensor.get_sensor_data_headers():
value = next(val_cycle)
if sensor_data_header == SensorData.HEADER_TIMESTAMP:
sensor_data.timestamp = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z")
wasss_app.logger.info(sensor_data.timestamp)
elif sensor_data_header == SensorData.HEADER_VOLTAGE:
sensor_data.voltage = value
elif sensor_data_header == SensorData.HEADER_TEMPERATURE:
sensor_data.temp = value
elif sensor_data_header == SensorData.HEADER_HUMIDITY:
sensor_data.humidity = value
elif sensor_data_header == SensorData.HEADER_PRESSURE:
sensor_data.pressure = value
elif sensor_data_header == SensorData.HEADER_BRIGHTNESS:
sensor_data.brightness = value
elif sensor_data_header == SensorData.HEADER_SPEED:
sensor_data.speed = value
elif sensor_data_header == SensorData.HEADER_MOTION:
sensor_data.motion = value
else:
gen_sen_data = GenericSensorData(sensor=sensor, name=sensor_data_header, value=value)
db.session.add(sensor_data)
db.session.commit()
return jsonify("ok")
api_v1.add_resource(SensorData_Handler, '/sensor/<string:mac>/data')

87
backend/api/group_api.py Normal file
View File

@@ -0,0 +1,87 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
from flask_jwt_extended import jwt_required
from flask_restplus import fields, Resource
from backend import db
from backend.api import api_group
from backend.models.user_model import Group
group_model = api_group.model('Group', {
'id': fields.String(required=False, description='The group\'s identifier'),
'name': fields.String(required=True, description='The group\'s name'),
'description': fields.String(required=False, description='The group\'s description'),
'users': fields.List(fields.Nested(api_group.model('group_member',
{'id': fields.Integer(), 'nickname': fields.String(),
'first_name': fields.String(), 'last_name': fields.String(),
'email': fields.String(), 'registered_on': fields.DateTime(),
'last_seen': fields.DateTime()})),
required=False, description='Group members.')
})
@api_group.route('/<int:id>')
@api_group.response(404, 'Group not found')
@api_group.param('id', 'The group identifier')
class GroupResource(Resource):
@jwt_required
@api_group.doc('get_group')
@api_group.marshal_with(group_model)
def get(self, id):
"""Fetch a user given its identifier"""
group = Group.get_by_id(id)
if group is not None:
return group
api_group.abort(404)
@jwt_required
@api_group.doc('delete_todo')
@api_group.response(204, 'Todo deleted')
def delete(self, id):
'''Delete a task given its identifier'''
group = Group.get_by_id(id)
if group is not None:
group.delete()
return '', 204
api_group.abort(404)
@jwt_required
@api_group.doc('update_group')
@api_group.expect(group_model)
@api_group.marshal_with(group_model)
def put(self, id):
'''Update a task given its identifier'''
group = Group.get_by_id(id)
if group is not None:
group.name = api_group["name"]
db.session.commit()
return group
api_group.abort(404)
@api_group.route('')
class GroupList(Resource):
@jwt_required
@api_group.doc('groups')
@api_group.marshal_list_with(group_model)
def get(self):
"""
List all groups
:return: groups
"""
return Group.get_all()
@jwt_required
@api_group.doc('create_group')
@api_group.expect(group_model)
@api_group.marshal_with(group_model, code=201)
def post(self):
group = Group(**api_group.payload)
db.session.add(group)
db.session.commit()
return group

275
backend/api/recorder_api.py Normal file
View File

@@ -0,0 +1,275 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
from datetime import datetime
from pprint import pprint
from flask_jwt_extended import jwt_required
from flask_restplus import fields, Resource, inputs
from backend import db, app
from backend.api import api_recorder
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
from backend.models.room_model import Room
import backend.recorder_adapters as r_a
recorder_model = api_recorder.model('Recorder', {
'id': fields.String(required=False, description='The recorder\'s identifier'),
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
'last_time_modified': fields.DateTime(required=False, description='Creation date of the recorder'),
'name': fields.String(min_length=3, required=True, description='The recorder\'s name'),
'description': fields.String(required=False, description='The recorder\'s description'),
'locked': fields.Boolean(required=False, description='Indicates whether the recorder settings can be altered'),
'lock_message': fields.String(required=False, description='Optional: message explaining lock state'),
'offline': fields.Boolean(required=False,
description='Should be set when recorder is disconnected for maintenance, etc.'),
'ip': fields.String(required=False, description='The recorder\'s IP address'),
'network_name': fields.String(required=False, description='The recorder\'s network name'),
'ssh_port': fields.Integer(required=True, default=22, description='The recorder\'s SSH port number'),
'telnet_port': fields.Integer(required=True, default=23, description='The recorder\'s telnet port number'),
# 'use_telnet_instead_ssh': fields.Boolean(required=False, default=False,
# description='If this is set, telnet will be used instead of ssh. '
# 'This might require specific commands.'),
'recorder_model': fields.Nested(api_recorder.model('recorder_model',
{'id': fields.Integer(),
'name': fields.String(attribute="model_name", )}),
required=False,
allow_null=True,
skip_none=False,
description='Model of the recorder.'),
'room': fields.Nested(api_recorder.model('recorder_room',
{'id': fields.Integer(), 'name': fields.String(),
'number': fields.String(), 'alternate_name': fields.String()}),
r0equired=False,
allow_null=True,
skip_none=False,
description='Room in which the recorder is located.'),
'virtual_commands': fields.List(fields.Nested(api_recorder.model('recorder_virtual_commands',
{'id': fields.Integer(),
'name': fields.String()})))
})
recorder_command_model = api_recorder.model('Recorder Command', {
'id': fields.String(required=False, description='The recorder command\'s identifier'),
'name': fields.String(required=True, description='The recorder command\'s name'),
'alternative_name': fields.String(required=False, description='The recorder command\'s alternative name'),
'disabled': fields.Boolean(required=False, description='Indicates if the recorder command is disabled'),
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
'last_time_modified': fields.DateTime(required=False),
'description': fields.String(required=False, description='The recorder command\'s description'),
'parameters': fields.Raw(required=True, description='The recorder parameters'),
'recorder_model': fields.Nested(api_recorder.model('recorder_command_models',
{'id': fields.Integer(),
'name': fields.String(attribute="model_name", )})),
})
recorder_model_model = api_recorder.model('Recorder Model', {
'id': fields.String(required=False, description='The recorder model\'s identifier'),
'name': fields.String(attribute="model_name", required=True, description='The recorder model\'s name'),
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
'last_time_modified': fields.DateTime(required=False),
'notes': fields.String(required=False, description='The recorder model\'s notes'),
'requires_username': fields.Boolean(),
'requires_password': fields.Boolean(),
'recorders': fields.List(fields.Nested(api_recorder.model('recorder_model',
{'id': fields.Integer(),
'name': fields.String(attribute="model_name", ),
'network_name': fields.String(),
'ip': fields.String()})), required=False,
description='Model of the recorder.'),
'commands': fields.List(fields.Nested(recorder_command_model), attribute="recorder_commands")
})
# ==
@api_recorder.route('/<int:id>')
@api_recorder.response(404, 'Recorder not found')
@api_recorder.param('id', 'The recorder identifier')
class RecorderResource(Resource):
@jwt_required
@api_recorder.doc('get_recorder')
@api_recorder.marshal_with(recorder_model, skip_none=False)
def get(self, id):
"""Fetch a recorder given its identifier"""
recorder = Recorder.query.get(id)
if recorder is not None:
return recorder
api_recorder.abort(404)
@jwt_required
@api_recorder.doc('delete_todo')
@api_recorder.response(204, 'Todo deleted')
def delete(self, id):
"""Delete a recorder given its identifier"""
recorder = Recorder.query.get(id)
if recorder is not None:
db.session.delete(recorder)
db.session.commit()
return '', 204
api_recorder.abort(404)
recorder_update_parser = api_recorder.parser()
recorder_update_parser.add_argument('name', type=str, required=False, nullable=False, store_missing=False)
recorder_update_parser.add_argument('network_name', type=inputs.regex(inputs.netloc_regex), required=False, store_missing=False)
recorder_update_parser.add_argument('ip', type=inputs.ipv4, required=False, store_missing=False)
recorder_update_parser.add_argument('ip6', type=inputs.ipv6, required=False, store_missing=False)
recorder_update_parser.add_argument('ssh_port', type=inputs.int_range(0,65535), required=False, default=22, store_missing=False)
recorder_update_parser.add_argument('telnet_port', type=inputs.int_range(0,65535), required=False, default=23, store_missing=False)
recorder_update_parser.add_argument('room_id', type=int, required=False, store_missing=False)
recorder_update_parser.add_argument('offline', type=inputs.boolean, required=False, default=False, store_missing=False)
recorder_update_parser.add_argument('locked', type=inputs.boolean, required=False, default=False, store_missing=False)
recorder_update_parser.add_argument('lock_message', type=str, required=False, nullable=True, default=None,
store_missing=False)
recorder_update_parser.add_argument('model_id', type=int, required=False, store_missing=False)
recorder_update_parser.add_argument('description', type=str, required=False, nullable=True, default=None,
store_missing=False)
recorder_update_parser.add_argument('virtual_command_ids', action='split', nullable=True, default=[],
required=False, store_missing=False)
@jwt_required
@api_recorder.doc('update_recorder')
@api_recorder.expect(recorder_model)
def put(self, id):
"""Update a recorder given its identifier"""
args = self.recorder_update_parser.parse_args(strict=True)
args['last_time_modified'] = datetime.utcnow()
pprint(args)
num_rows_matched = Recorder.query.filter_by(id=id).update(args)
print(num_rows_matched)
if num_rows_matched < 1:
api_recorder.abort(404)
db.session.commit()
return "ok"
@api_recorder.route('')
class RecorderList(Resource):
@jwt_required
@api_recorder.doc('recorders')
@api_recorder.marshal_list_with(recorder_model, skip_none=False)
def get(self):
"""
List all recorders
:return: recorders
"""
return Recorder.get_all()
@jwt_required
@api_recorder.doc('create_recorder')
@api_recorder.expect(recorder_model)
@api_recorder.marshal_with(recorder_model, skip_none=False, code=201)
def post(self):
if "room_id" in api_recorder.payload:
if api_recorder.payload["room_id"] is None:
api_recorder.payload["room"] = None
else:
room = Room.query.get(api_recorder.payload["room_id"])
if room is not None:
api_recorder.payload["room"] = room
else:
return "specified room (id: {}) does not exist!".format(api_recorder.payload["room_id"]), 404
if "recorder_model_id" in api_recorder.payload:
if api_recorder.payload["recorder_model_id"] is None:
api_recorder.payload["recorder_model"] = None
else:
rec_model = RecorderModel.query.get(api_recorder.payload["recorder_model_id"])
if rec_model is not None:
api_recorder.payload["recorder_model"] = rec_model
else:
return "specified recorder model (id: {}) does not exist!".format(
api_recorder.payload["recorder_model_id"]), 404
recorder = Recorder(**api_recorder.payload)
db.session.add(recorder)
db.session.commit()
return recorder
@api_recorder.route('/model/<int:id>')
@api_recorder.response(404, 'Recorder Model not found')
@api_recorder.param('id', 'The recorder model identifier')
class RecorderModelResource(Resource):
@jwt_required
@api_recorder.doc('get_recorder_model')
@api_recorder.marshal_with(recorder_model_model)
def get(self, id):
"""Fetch a recorder model given its identifier"""
recorder_model = RecorderModel.query.get(id)
if recorder_model is not None:
return recorder_model
api_recorder.abort(404)
recorder_model_parser = api_recorder.parser()
recorder_model_parser.add_argument('notes', type=str, required=True)
@jwt_required
@api_recorder.doc('update_recorder_model')
@api_recorder.expect(recorder_model_parser)
@api_recorder.marshal_with(recorder_model_model)
def put(self, id):
"""Update a recorder_model given its identifier"""
num_rows_matched = RecorderModel.query.filter_by(id=id).update(api_recorder.payload)
if num_rows_matched < 1:
api_recorder.abort(404)
db.session.commit()
return "ok"
@api_recorder.route('/model')
class RecorderModelList(Resource):
@jwt_required
@api_recorder.doc('recorders')
@api_recorder.marshal_list_with(recorder_model_model)
def get(self):
return RecorderModel.get_all()
@api_recorder.route('/command/<int:id>')
@api_recorder.response(404, 'Recorder Command not found')
@api_recorder.param('id', 'The recorder command identifier')
class RecorderCommandResource(Resource):
@jwt_required
@api_recorder.doc('get_recorder_command')
@api_recorder.marshal_with(recorder_command_model)
def get(self, id):
"""Fetch a recorder command given its identifier"""
recorder_command = RecorderCommand.query.get(id)
if recorder_command is not None:
return recorder_command
api_recorder.abort(404)
recorder_command_model_parser = api_recorder.parser()
recorder_command_model_parser.add_argument('description', type=str, required=False)
recorder_command_model_parser.add_argument('alternative_name', type=str, required=False)
@jwt_required
@api_recorder.doc('update_recorder_command')
@api_recorder.expect(recorder_command_model_parser)
@api_recorder.marshal_with(recorder_command_model)
def put(self, id):
"""Update a recorder command given its identifier"""
num_rows_matched = RecorderCommand.query.filter_by(id=id).update(api_recorder.payload)
if num_rows_matched < 1:
api_recorder.abort(404)
db.session.commit()
return "ok"
@api_recorder.route('/command')
class RecorderCommandList(Resource):
@jwt_required
@api_recorder.doc('recorder_commands')
@api_recorder.marshal_list_with(recorder_command_model)
def get(self):
"""
List all recorders commands
:return: recorder commands
"""
return RecorderCommand.get_all()

122
backend/api/room_api.py Normal file
View File

@@ -0,0 +1,122 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
from flask_jwt_extended import jwt_required
from flask_restplus import fields, Resource
from sqlalchemy import exc
from backend import db, app
from backend.api import api_room
from backend.models.room_model import Room
from backend.models.recorder_model import Recorder
room_model = api_room.model('Room', {
'id': fields.String(required=False, description='The room\'s identifier'),
'created_at': fields.DateTime(required=False, description='Creation date of the room info'),
'name': fields.String(required=True, description='The room\'s name'),
'alternate_name': fields.String(required=False, description='The room\'s alternate name'),
'comment': fields.String(required=False, description='The room\'s comment'),
'number': fields.String(required=True, description='The room\'s number'),
'building_name': fields.String(required=False, description='The building\'s name'),
'building_number': fields.String(required=False, description='The building\'s number'),
'recorder': fields.Nested(api_room.model('room_recorder',
{'id': fields.Integer(), 'name': fields.String(),
'ip': fields.String(), 'network_name': fields.String()}),
allow_null=True,
skip_none=False,
required=False,
description='Room recorder.'),
})
@api_room.route('/<int:id>')
@api_room.response(404, 'Room not found')
@api_room.param('id', 'The room identifier')
class RoomResource(Resource):
@jwt_required
@api_room.doc('get_room')
@api_room.marshal_with(room_model, skip_none=False)
def get(self, id):
"""Fetch a user given its identifier"""
room = Room.query.get(id)
if room is not None:
return room
api_room.abort(404)
@jwt_required
@api_room.doc('delete_todo')
@api_room.response(204, 'Todo deleted')
def delete(self, id):
'''Delete a task given its identifier'''
room = Room.query.get(id)
if room is not None:
db.session.delete(room)
db.session.commit()
return '', 204
api_room.abort(404)
@jwt_required
@api_room.doc('update_room')
@api_room.expect(room_model)
def put(self, id):
app.logger.debug(api_room.payload)
'''Update a task given its identifier'''
if "recorder_id" in api_room.payload:
if api_room.payload["recorder_id"] is None:
api_room.payload["recorder"] = None
else:
recorder = Recorder.query.get(api_room.payload["recorder_id"])
if recorder is not None:
api_room.payload["recorder"] = recorder
else:
return "specified recorder (id: {}) does not exist!".format(api_room.payload["recorder_id"]), 404
room = Room.query.get(id)
if room is not None:
room.recorder = api_room.payload["recorder"]
else:
num_rows_matched = Room.query.filter_by(id=id).update(api_room.payload)
db.session.commit()
return "ok"
api_room.abort(404)
@api_room.route('')
class RoomList(Resource):
@jwt_required
@api_room.doc('rooms')
@api_room.marshal_list_with(room_model, skip_none=False)
def get(self):
"""
List all rooms
:return: rooms
"""
return Room.get_all()
@jwt_required
@api_room.doc('create_room')
@api_room.expect(room_model)
@api_room.marshal_with(room_model, skip_none=False, code=201)
def post(self):
if "recorder_id" in api_room.payload:
if api_room.payload["recorder_id"] is None:
api_room.payload["recorder"] = None
else:
recorder = Recorder.query.get(api_room.payload["recorder_id"])
if recorder is not None:
api_room.payload["recorder"] = recorder
else:
return "specified recorder (id: {}) does not exist!".format(api_room.payload["recorder_id"]), 404
del api_room.payload["recorder_id"]
room = Room(**api_room.payload)
db.session.add(room)
try:
db.session.commit()
return room
except exc.IntegrityError as e:
db.session.rollback()
return str(e.detail), 400

107
backend/api/user_api.py Normal file
View File

@@ -0,0 +1,107 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
from datetime import datetime
from pprint import pprint
from flask_jwt_extended import get_jwt_identity, jwt_required, current_user
from flask_restplus import Resource, fields, inputs
from backend import db, app, jwt_auth
from backend.api import api_user
from backend.models.user_model import User, Group
user_model = api_user.model('User', {
'id': fields.String(required=True, description='The user\'s identifier'),
'first_name': fields.String(required=True, description='The user\'s first name'),
'last_name': fields.String(required=True, description='The user\'s last name'),
'email': fields.String(required=True, description='The user\'s email address'),
'nickname': fields.String(required=False, description='The user\'s nick name'),
'groups': fields.List(
fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})),
required=False, description='Group memberships.'),
})
user_update_parser = api_user.parser()
user_update_parser.add_argument('email', type=inputs.email, required=False, nullable=False, store_missing=False)
user_update_parser.add_argument('nickname', type=str, required=False, store_missing=False)
user_update_parser.add_argument('first_name', type=str, required=False, store_missing=False)
user_update_parser.add_argument('last_name', type=str, required=False, store_missing=False)
@api_user.route('/profile')
class Profile(Resource):
@jwt_required
@api_user.marshal_with(user_model)
def get(self):
"""Get infos about logged in user."""
current_user_id = get_jwt_identity()
app.logger.info(current_user_id)
return User.get_by_identifier(current_user_id)
@jwt_required
@api_user.expect(user_update_parser)
def put(self):
args = user_update_parser.parse_args()
args['last_time_modified'] = datetime.utcnow()
pprint(args)
print(current_user)
num_rows_matched = User.query.filter_by(id=current_user.id).update(args)
print(num_rows_matched)
if num_rows_matched < 1:
api_user.abort("Nothing has been updated!")
db.session.commit()
return "ok"
@api_user.route('')
class UserList(Resource):
"""
This is a test class.
"""
# @jwt_auth.login_required
@jwt_required
@api_user.doc('users')
@api_user.marshal_list_with(user_model)
def get(self):
"""
just a test!
:return: Hello: World
"""
current_user = get_jwt_identity()
app.logger.info(current_user)
return User.get_all()
@jwt_required
@api_user.doc('create_group')
@api_user.expect(user_model)
@api_user.marshal_with(user_model, code=201)
def post(self):
user = User(**api_user.payload)
db.session.add(user)
db.session.commit()
return user
@api_user.route('/<id>')
@api_user.param('id', 'The user identifier')
@api_user.response(404, 'User not found')
class UserResource(Resource):
@jwt_auth.login_required
@api_user.doc('get_user')
@api_user.marshal_with(user_model)
def get(self, id):
"""Fetch a user given its identifier"""
user = User.get_by_id(id)
if user is not None:
return user
api_user.abort(404)
# api_user.add_resource(UserResource, '/')

View File

@@ -0,0 +1,134 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to authentication through the API.
For example: listing of available auth providers or registration of users.
Login through API does not start a new session, but instead returns JWT.
"""
import inspect
import pkgutil
from pprint import pprint
from flask_jwt_extended import jwt_required
from flask_restplus import fields, Resource
from backend import db, app
from backend.api import api_virtual_command
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
from backend.models.room_model import Room
import backend.recorder_adapters as r_a
virtual_command_model = api_virtual_command.model('VirtualCommand', {
'id': fields.String(required=False, description='The recorder\'s identifier'),
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
'name': fields.String(min_length=3, required=True, description='The recorder\'s name'),
'description': fields.String(required=False, description='The recorder\'s description'),
'parent_virtual_command': fields.Nested(api_virtual_command.model('VirtualCommandParent',
{
'id': fields.String(required=False,
description='The recorder\'s identifier'),
'name': fields.String(min_length=3,
required=True,
description='The recorder\'s name'),
},
required=False,
allow_null=True,
skip_none=False,
description='Parent virtual command.')),
'room': fields.Nested(api_virtual_command.model('recorder_room',
{'id': fields.Integer(), 'name': fields.String(),
'number': fields.String(), 'alternate_name': fields.String()}),
r0equired=False,
allow_null=True,
skip_none=False,
description='Room in which the recorder is located.')
})
# ==
@api_virtual_command.route('/<int:id>')
@api_virtual_command.response(404, 'Recorder not found')
@api_virtual_command.param('id', 'The recorder identifier')
class VirtualCommandResource(Resource):
@jwt_required
@api_virtual_command.doc('get_recorder')
@api_virtual_command.marshal_with(virtual_command_model, skip_none=False)
def get(self, id):
"""Fetch a recorder given its identifier"""
recorder = Recorder.query.get(id)
if recorder is not None:
return recorder
api_virtual_command.abort(404)
@jwt_required
@api_virtual_command.doc('delete_todo')
@api_virtual_command.response(204, 'Todo deleted')
def delete(self, id):
"""Delete a recorder given its identifier"""
recorder = Recorder.query.get(id)
if recorder is not None:
db.session.delete(recorder)
db.session.commit()
return '', 204
api_virtual_command.abort(404)
virtual_command_model_parser = api_virtual_command.parser()
virtual_command_model_parser.add_argument('notes', type=str, required=True)
@jwt_required
@api_virtual_command.doc('update_recorder')
@api_virtual_command.expect(virtual_command_model_parser)
def put(self, id):
"""Update a recorder given its identifier"""
num_rows_matched = Recorder.query.filter_by(id=id).update(api_virtual_command.payload)
if num_rows_matched < 1:
api_virtual_command.abort(404)
db.session.commit()
return "ok"
@api_virtual_command.route('')
class RecorderList(Resource):
@jwt_required
@api_virtual_command.doc('recorders')
@api_virtual_command.marshal_list_with(virtual_command_model, skip_none=False)
def get(self):
"""
List all recorders
:return: recorders
"""
return Recorder.get_all()
virtual_command_model_parser = api_virtual_command.parser()
virtual_command_model_parser.add_argument('notes', type=str, required=True)
@jwt_required
@api_virtual_command.doc('create_recorder')
@api_virtual_command.expect(virtual_command_model_parser)
@api_virtual_command.marshal_with(virtual_command_model, skip_none=False, code=201)
def post(self):
if "room_id" in api_virtual_command.payload:
if api_virtual_command.payload["room_id"] is None:
api_virtual_command.payload["room"] = None
else:
room = Room.query.get(api_virtual_command.payload["room_id"])
if room is not None:
api_virtual_command.payload["room"] = room
else:
return "specified room (id: {}) does not exist!".format(api_virtual_command.payload["room_id"]), 404
if "recorder_model_id" in api_virtual_command.payload:
if api_virtual_command.payload["recorder_model_id"] is None:
api_virtual_command.payload["recorder_model"] = None
else:
rec_model = RecorderModel.query.get(api_virtual_command.payload["recorder_model_id"])
if rec_model is not None:
api_virtual_command.payload["recorder_model"] = rec_model
else:
return "specified recorder model (id: {}) does not exist!".format(
api_virtual_command.payload["recorder_model_id"]), 404
recorder = Recorder(**api_virtual_command.payload)
db.session.add(recorder)
db.session.commit()
return recorder

78
backend/auth/__init__.py Normal file
View File

@@ -0,0 +1,78 @@
# Copyright (c) 2019. Tobias Kurze
"""
Base module for auth aspects.
Also this module contains mainly code for login through HTML pages served by the backend.
If frontend pages are build by frontend code (JS, etc.) authentication should consider using api functions.
(For more info, see api.auth_api.py.)
This code uses login_user and logout user (to start and end sessions) ... API code returns JWTs.
"""
from flask import Blueprint, jsonify, url_for
from flask_login import logout_user, LoginManager
from werkzeug.routing import BuildError
from backend import jwt_extended
from backend.models import BlacklistToken, User
auth_bp = Blueprint('auth', __name__, url_prefix='/auth', template_folder='templates')
from backend.auth.config import AUTH_PROVIDERS, DEFAULT_FRONTEND_PROVIDER
from backend.auth.oidc_config import OIDC_PROVIDERS
from backend.auth.oidc import oidc_auth
from .basic_auth import *
def auth_decorator(): # custom decorator
pass
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
try:
prov = AUTH_PROVIDERS[DEFAULT_FRONTEND_PROVIDER]
except KeyError:
return "No known default provider specified!"
url = prov["url"]
try:
url = url_for(prov["url"], next=request.endpoint)
except BuildError as e:
pass
#logger.log("Can't create endpoint for '{}' (specified provider: {}).".format(e.endpoint, DEFAULT_PROVIDER))
return redirect(url)
@auth_bp.route('/login_select', methods=['GET'])
def login_select():
return render_template('login_select.html', providers=AUTH_PROVIDERS)
@auth_bp.route('/logout', methods=('GET', ))
def logout():
logout_user()
@jwt_extended.user_claims_loader
def add_claims_to_access_token(user):
if isinstance(user, str):
return {}
return {'role': user.role, 'groups': [g.to_dict() for g in user.groups]}
@jwt_extended.user_identity_loader
def user_identity_loader(user):
return user.email
@jwt_extended.user_loader_callback_loader
def user_loader_callback(identity):
print("### user_loader_callback_loader")
return User.get_by_identifier(identity)
@jwt_extended.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
jti = decrypted_token['jti']
return BlacklistToken.get_by_token(jti) is not None

View File

@@ -0,0 +1,22 @@
# Route for handling the login page logic
from flask import request, redirect, render_template
from flask_login import login_user
from backend.auth import auth_bp
from backend.models.user_model import User
@auth_bp.route('/base_login', methods=['GET', 'POST'])
def base_login():
error = None
if request.method == 'POST':
user = User.authenticate(email=request.form['email'], password=request.form['password'])
if user is None:
error = 'Invalid Credentials. Please try again.'
else:
login_user(user)
return redirect("/")
return render_template('login.html', error=error)

29
backend/auth/config.py Normal file
View File

@@ -0,0 +1,29 @@
from typing import Dict, List
AUTH_PROVIDERS: Dict[str, Dict[str, str]] = {
"KIT OIDC":
{
"type": "oidc",
"url": "auth_api.oidc"
},
"Base Login":
{
"type": "login_form",
"url": "auth.base_login"
},
"KIT OIDC (API)":
{
"type": "api_oidc",
"url": "auth_api.oidc"
},
"User-Password (API)":
{
"type": "api_login_form",
"url": "auth_api.login"
},
}
#DEFAULT_PROVIDER: str = "Base Login"
DEFAULT_PROVIDER: str = "KIT OIDC (API)"
DEFAULT_FRONTEND_PROVIDER: str = "Base Login"

77
backend/auth/oidc.py Normal file
View File

@@ -0,0 +1,77 @@
# Copyright (c) 2019. Tobias Kurze
"""
OIDC login auth module
"""
import flask
from flask import jsonify, redirect, url_for
from flask_login import login_user
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
from flask_pyoidc.user_session import UserSession
from backend import app, db
from backend.models.user_model import User
from . import auth_bp
from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS
OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth
OIDCAuthentication.oidc_logout_orig = OIDCAuthentication.oidc_logout
def oidc_auth_default_provider(self):
"""monkey patch oidc_auth"""
return self.oidc_auth_orig(PROVIDER_NAME)
def oidc_logout_default_provider(self):
"""monkey patch oidc_logout"""
return self.oidc_logout_orig(PROVIDER_NAME)
OIDCAuthentication.oidc_auth = oidc_auth_default_provider
OIDCAuthentication.oidc_logout = oidc_logout_default_provider
oidc_auth = OIDCAuthentication(OIDC_PROVIDERS)
def create_or_retrieve_user_from_userinfo(userinfo):
"""Updates and returns ar creates a user from userinfo (part of OIDC token)."""
try:
email = userinfo["email"]
except KeyError:
return None
user = User.get_by_identifier(email)
if user is not None:
app.logger.info("user found")
#TODO: update user!
return user
user = User(email=email, first_name=userinfo.get("given_name", ""),
last_name=userinfo.get("family_name", ""))
app.logger.info("creating new user")
db.session.add(user)
db.session.commit()
return user
@auth_bp.route('/oidc', methods=['GET'])
@oidc_auth.oidc_auth()
def oidc():
user_session = UserSession(flask.session)
app.logger.info(user_session.userinfo)
user = create_or_retrieve_user_from_userinfo(user_session.userinfo)
login_user(user)
return jsonify(id_token=user_session.id_token,
access_token=flask.session['access_token'],
userinfo=user_session.userinfo)
@auth_bp.route('/oidc_logout', methods=['GET'])
def oidc_logout():
oidc_auth.oidc_logout()
return redirect('/')

View File

@@ -0,0 +1,15 @@
# Copyright (c) 2019. Tobias Kurze
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
REG_RESPONSE_CLIENT_ID = "lrc-test-bibliothek-kit-edu"
REG_RESPONSE_CLIENT_SECRET = "d8531b30-0e6b-4280-b611-1e6c8b4911fa"
CLIENT_METADATA = ClientMetadata(REG_RESPONSE_CLIENT_ID, REG_RESPONSE_CLIENT_SECRET)
PROVIDER_URL = "https://oidc.scc.kit.edu/auth/realms/kit"
PROVIDER_NAME = 'kit_oidc'
PROVIDER_CONFIG = ProviderConfiguration(issuer=PROVIDER_URL,
client_metadata=CLIENT_METADATA,
auth_request_params={'scope': ['openid', 'email', 'profile']})
OIDC_PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG}

View File

@@ -0,0 +1,23 @@
<html>
<head>
<title>Flask Intro - login page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="container">
<h1>Please login</h1>
<br>
<form action="" method="post">
<input type="text" placeholder="E-Mail" name="email" value="{{
request.form.username }}">
<input type="password" placeholder="Password" name="password" value="{{
request.form.password }}">
<input class="btn btn-default" type="submit" value="Login">
</form>
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}
{% endif %}
</div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<html>
<head>
<title>Flask Intro - login page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="container">
<h1>Please select login method</h1>
<br>
<ul>
{% for provider in providers %}
<li><a href="{{url_for(providers[provider].url)}}">{{ provider }} ({{ providers[provider].type }})</a></li>
{% endfor %}
</ul>
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}
{% endif %}
</div>
</body>
</html>

42
backend/auth/utils.py Normal file
View File

@@ -0,0 +1,42 @@
import flask_jwt_extended
from flask_jwt_extended import jwt_optional, get_jwt_identity
from functools import wraps
from backend import jwt_auth
from backend.models.user_model import User
def requires_permission_level(permission_level):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if flask_jwt_extended.verify_jwt_in_request():
current_user_id = get_jwt_identity()
user = User.get_by_identifier(current_user_id)
if user is not None:
if user.has_permission(permission_level):
#for g in user.groups:
# if g.permissions
#TODO
pass
else:
pass
# return FALSE
#if not session.get('email'):
# return redirect(url_for('users.login'))
#user = User.find_by_email(session['email'])
#elif not user.allowed(access_level):
# return redirect(url_for('users.profile', message="You do not have access to that page. Sorry!"))
return f(*args, **kwargs)
return decorated_function
return decorator
def require_jwt():
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return jwt_auth.login_required(jwt_optional(f(*args, **kwargs)))
return decorated_function
return decorator

113
backend/config.py Normal file
View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# ...
# available languages
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config():
# SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de:5443"
# SERVER_NAME = "localhost.dev"
SERVER_NAME = "localhost:5443"
# SERVER_NAME = "localhost"
# SERVER_NAME = "localhost.localdomain"
# PORT = 5443
# PREFERRED_URL_SCHEME = 'https'
TEMPLATE_AUTO_RELOAD = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
SQLALCHEMY_TRACK_MODIFICATIONS = True
WTF_CSRF_ENABLED = True
SECRET_KEY = 'you-will-never-guess'
OPENID_PROVIDERS = [
{'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'},
{'name': 'Yahoo', 'url': 'https://me.yahoo.com'},
{'name': 'AOL', 'url': 'http://openid.aol.com/<username>'},
{'name': 'Flickr', 'url': 'http://www.flickr.com/<username>'},
{'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}]
OAUTH_CREDENTIALS = {
'facebook': {
'id': '1198624176930248',
'secret': '4fbc01d776834c1ffc89a5bed1cd97d0'
},
'twitter': {
'id': '3RzWQclolxWZIMq5LJqzRZPTl',
'secret': 'm9TEd58DSEtRrZHpz2EjrV9AhsBRxKMo8m3kuIZj3zLwzwIimt'
},
'google': {
'id': '1084993305658-d9n88548ssrtmt5v6s2dne57i4qpviur.apps.googleusercontent.com',
'secret': 'oNpvoAKMPMjRyiu5EDrmmX4X'
},
}
# mail server settings
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
MAIL_USERNAME = None
MAIL_PASSWORD = None
# administrator list
ADMINS = ['you@example.com']
# pagination
POSTS_PER_PAGE = 5
LOCKS_PER_PAGE = 8
LANGUAGES = {
'en': 'English',
'es': 'Español'
}
# ASSETS_DEBUG = True
# JWT_SECRET = "abcxyz"
# JWT_ALGORITHM = "HS256"
# JWT_EXP_DELTA_SECONDS = 5 * 60
JWT_SECRET_KEY = "abcxyz"
JWT_BLACKLIST_ENABLED = True
JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh']
AUTH_RETURN_EXTERNAL_JWT = False
INDEX_TEMPLATE = "index.html"
# # INITIAL VALUES # #
PERMISSIONS = ["RECODER_NEW", "RECORDER_EDIT", "RECODER_SHOW", "RECORDER_DELETE",
"RECORDER_COMMAND_EXECUTE", "RECORDER_COMMAND_EDIT_ACL",
"VIRTUAL_COMMAND_CREATE", "VIRTUAL_COMMAND_EDIT", "VIRTUAL_COMMAND_SHOW", "VIRTUAL_COMMAND_DELETE",
"CRON_JOB_CREATE", "CRON_JOB_EDIT", "CRON_JOB_SHOW", "CRON_JOB_DELETE"]
GROUPS = [ #{"name": "Admins",
#"permissions": PERMISSIONS},
{"name": "ZML"},
{"name": "read_only"}]
USERS = [{"nickname": "admin",
"first_name": "tobias",
"last_name": "kurze",
"email": "kurze@kit.edu",
"role": "admin",
"password": "admin"}
]
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config):
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db_debug')
SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de"
PORT = 5443
DEBUG = True
class TestingConfig(Config):
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db_test')
TESTING = True

75
backend/manage.py Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python
import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir))
import os
import unittest
import coverage
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from backend import app, db
COV = coverage.coverage(
branch=True,
include='app/*',
omit=[
'app/tests/*',
'app/server/config.py',
'app/server/*/__init__.py'
]
)
COV.start()
migrate = Migrate(app, db)
manager = Manager(app)
# migrations
manager.add_command('db', MigrateCommand)
@manager.command
def test():
"""Runs the unit tests without test coverage."""
tests = unittest.TestLoader().discover('tests', pattern='test*.py')
result = unittest.TextTestRunner(verbosity=2).run(tests)
if result.wasSuccessful():
return 0
return 1
@manager.command
def cov():
"""Runs the unit tests with coverage."""
tests = unittest.TestLoader().discover('app/tests')
result = unittest.TextTestRunner(verbosity=2).run(tests)
if result.wasSuccessful():
COV.stop()
COV.save()
print('Coverage Summary:')
COV.report()
basedir = os.path.abspath(os.path.dirname(__file__))
covdir = os.path.join(basedir, 'tmp/coverage')
COV.html_report(directory=covdir)
print('HTML version: file://%s/index.html' % covdir)
COV.erase()
return 0
return 1
@manager.command
def create_db():
"""Creates the db tables."""
db.create_all()
@manager.command
def drop_db():
"""Drops the db tables."""
db.drop_all()
if __name__ == '__main__':
manager.run()

View File

@@ -0,0 +1 @@
Generic single-database configuration.

View File

@@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

95
backend/migrations/env.py Normal file
View File

@@ -0,0 +1,95 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,136 @@
"""empty message
Revision ID: b154e49921e0
Revises:
Create Date: 2019-04-11 14:44:25.836132
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b154e49921e0'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('blacklist_tokens',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('token', sa.String(length=500), nullable=False),
sa.Column('blacklisted_on', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token')
)
op.create_table('example_data_item',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('mac', sa.String(length=32), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('some_string_value', sa.String(), nullable=True),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('description', sa.String(length=4096), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_example_data_item_mac'), 'example_data_item', ['mac'], unique=True)
op.create_index(op.f('ix_example_data_item_name'), 'example_data_item', ['name'], unique=False)
op.create_index(op.f('ix_example_data_item_some_string_value'), 'example_data_item', ['some_string_value'], unique=False)
op.create_index(op.f('ix_example_data_item_uuid'), 'example_data_item', ['uuid'], unique=True)
op.create_table('group',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.Unicode(length=63), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('permission',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.Unicode(length=63), nullable=False),
sa.Column('description', sa.Unicode(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('group_permission',
sa.Column('group_id', sa.Integer(), nullable=False),
sa.Column('permission_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['group.id'], onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('group_id', 'permission_id')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('social_id', sa.Unicode(length=63), nullable=True),
sa.Column('nickname', sa.Unicode(length=63), nullable=True),
sa.Column('first_name', sa.Unicode(length=63), nullable=True),
sa.Column('last_name', sa.Unicode(length=63), nullable=True),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('lang', sa.Unicode(length=32), nullable=True),
sa.Column('timezone', sa.Unicode(length=63), nullable=True),
sa.Column('example_data_item_id', sa.Integer(), nullable=True),
sa.Column('about_me', sa.Unicode(length=255), nullable=True),
sa.Column('role', sa.Unicode(length=63), nullable=True),
sa.Column('password', sa.String(length=255), nullable=True),
sa.Column('registered_on', sa.DateTime(), nullable=False),
sa.Column('external_user', sa.Boolean(), nullable=True),
sa.Column('last_seen', sa.DateTime(), nullable=True),
sa.Column('jwt_exp_delta_seconds', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['example_data_item_id'], ['example_data_item.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('social_id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_first_name'), 'user', ['first_name'], unique=False)
op.create_index(op.f('ix_user_last_name'), 'user', ['last_name'], unique=False)
op.create_index(op.f('ix_user_nickname'), 'user', ['nickname'], unique=True)
op.create_table('acquaintances',
sa.Column('me_id', sa.Integer(), nullable=True),
sa.Column('acquaintance_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['acquaintance_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['me_id'], ['user.id'], )
)
op.create_table('followers',
sa.Column('follower_id', sa.Integer(), nullable=True),
sa.Column('followed_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['follower_id'], ['user.id'], )
)
op.create_table('post',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('body', sa.String(length=140), nullable=True),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user_group',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('group_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['group.id'], onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'group_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_group')
op.drop_table('post')
op.drop_table('followers')
op.drop_table('acquaintances')
op.drop_index(op.f('ix_user_nickname'), table_name='user')
op.drop_index(op.f('ix_user_last_name'), table_name='user')
op.drop_index(op.f('ix_user_first_name'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')
op.drop_table('group_permission')
op.drop_table('permission')
op.drop_table('group')
op.drop_index(op.f('ix_example_data_item_uuid'), table_name='example_data_item')
op.drop_index(op.f('ix_example_data_item_some_string_value'), table_name='example_data_item')
op.drop_index(op.f('ix_example_data_item_name'), table_name='example_data_item')
op.drop_index(op.f('ix_example_data_item_mac'), table_name='example_data_item')
op.drop_table('example_data_item')
op.drop_table('blacklist_tokens')
# ### end Alembic commands ###

View File

@@ -0,0 +1,9 @@
"""
Import all models...
"""
from backend.models.example_model import *
from backend.models.user_model import *
from backend.models.post_model import *
from backend.models.recorder_model import *
from backend.models.room_model import *
from backend.models.virtual_command_model import *

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from backend import db
import uuid
class ExampleDataItem(db.Model):
"""
just an example class...
"""
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
mac = db.Column(db.String(32), nullable=False, unique=True, index=True)
uuid = db.Column(db.String(36), nullable=False, unique=True, index=True, default=str(uuid.uuid4()))
some_string_value = db.Column(db.String, nullable=True, index=True)
name = db.Column(db.String(128), default="<not set>", nullable=False, index=True, unique=False)
description = db.Column(db.String(4096), nullable=True, unique=False)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Example post model and related models
"""
from backend import db
class Post(db.Model):
"""
A post example class
"""
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return '<Post %r>' % self.body

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
"""
Models for lecture recorder
"""
import json
from sqlalchemy import MetaData
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import validates
from backend import db, app, login_manager
import re
from sqlalchemy import or_
from datetime import datetime, timedelta
from backend.models.virtual_command_model import virtual_command_recorder_command_table, virtual_command_recorder_table
metadata = MetaData()
class RecorderModel(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
last_time_modified = db.Column(db.DateTime, nullable=True, default=None)
record_adapter_id = db.Column(db.Unicode(63), unique=True, nullable=False)
model_name = db.Column(db.Unicode(63), unique=True, nullable=False)
notes = db.Column(db.Unicode(255), unique=False, nullable=True, default=None)
recorder_commands = db.relationship('RecorderCommand', back_populates='recorder_model')
recorders = db.relationship('Recorder', back_populates='recorder_model')
checksum = db.Column(db.String(63), unique=True, nullable=False)
_requires_user = db.Column(db.Integer, default=False, name='requires_user')
_requires_password = db.Column(db.Integer, default=True, name='requires_password')
@staticmethod
def get_all():
return RecorderModel.query.all()
@staticmethod
def get_by_name(name):
return RecorderModel.query.filter(RecorderModel.model_name == name).first()
@staticmethod
def get_by_adapter_id(name):
return RecorderModel.query.filter(RecorderModel.record_adapter_id == name).first()
@staticmethod
def get_by_checksum(md5_sum):
return RecorderModel.query.filter(RecorderModel.checksum == md5_sum).first()
@hybrid_property
def requires_user(self):
return self._requires_user > 0
@requires_user.setter
def requires_user(self, val: bool):
self._requires_user = 1 if val else 0
@hybrid_property
def requires_password(self):
return self._requires_password > 0
@requires_password.setter
def requires_password(self, val: bool):
self._requires_password = 1 if val else 0
class Recorder(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
last_time_modified = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
name = db.Column(db.Unicode(63), unique=True, nullable=False)
locked = db.Column(db.Boolean, default=False)
lock_message = db.Column(db.String, nullable=True, default=None)
offline = db.Column(db.Boolean, default=False)
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
room_id = db.Column(db.Integer, db.ForeignKey('room.id'))
room = db.relationship('Room', uselist=False, back_populates='recorder') # one-to-one relation (uselist=False)
ip = db.Column(db.String(15), unique=True, nullable=True, default=None)
ip6 = db.Column(db.String(46), unique=True, nullable=True, default=None)
network_name = db.Column(db.String(127), unique=True, nullable=True, default=None)
telnet_port = db.Column(db.Integer, unique=False, nullable=False, default=23)
ssh_port = db.Column(db.Integer, unique=False, nullable=False, default=22)
username = db.Column(db.String, nullable=True, default=None)
password = db.Column(db.String, nullable=True, default=None)
recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id'))
recorder_model = db.relationship('RecorderModel', back_populates='recorders')
virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_table, back_populates='recorders')
def __init__(self, **kwargs):
super(Recorder, self).__init__(**kwargs)
@staticmethod
def get_by_name(name):
return Recorder.query.filter(Recorder.name == name).first()
@staticmethod
def get_all():
return Recorder.query.all()
@validates('name')
def validate_name(self, key, value):
assert len(value) > 2
return value
def __str__(self):
return self.name
def to_dict(self):
return dict(id=self.id, name=self.name)
def toJSON(self):
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
sort_keys=True, indent=4)
class RecorderCommand(db.Model):
"""Table containing permissions associated with groups."""
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
last_time_modified = db.Column(db.DateTime, nullable=True, default=None)
name = db.Column(db.Unicode(63), unique=True, nullable=False)
alternative_name = db.Column(db.Unicode(63), unique=True, nullable=True, default=None)
disabled = db.Column(db.Boolean, default=False)
description = db.Column(db.Unicode(511), nullable=True, default=None)
parameters_string = db.Column(db.String(2047), nullable=True)
recorder_model = db.relationship('RecorderModel', back_populates='recorder_commands')
recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id'))
virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_command_table,
back_populates='recorder_commands')
@staticmethod
def get_all():
return RecorderCommand.query.all()
@property
def parameters(self):
if self.parameters_string is None:
return None
return json.loads(self.parameters_string)
@parameters.setter
def parameters(self, parameters_dict: dict):
self.parameters_string = json.dumps(parameters_dict)

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
Models for lecture recorder
"""
import json
from sqlalchemy import MetaData, CheckConstraint
from sqlalchemy.exc import IntegrityError
from datetime import datetime, timedelta
from backend import db, app, login_manager
from backend.tools.scrape_rooms import scrape_rooms
metadata = MetaData()
class Room(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
name = db.Column(db.Unicode(127), unique=False, nullable=False)
alternate_name = db.Column(db.Unicode(127), unique=False, nullable=True, default=None)
comment = db.Column(db.Unicode(2047), unique=False, nullable=True, default="")
number = db.Column(db.Unicode(63), unique=False, nullable=True)
building_name = db.Column(db.Unicode(63), unique=False, nullable=True)
building_number = db.Column(db.Unicode(63), unique=False, nullable=True)
recorder = db.relationship('Recorder', uselist=False, back_populates='room') # one-to-one relation (uselist=False)
__table_args__ = (
CheckConstraint('length(name) > 2',
name='name_min_length'),
)
def __init__(self, **kwargs):
super(Room, self).__init__(**kwargs)
@staticmethod
def get_by_name(name):
"""
Find group by name
:param name:
:return:
"""
return Room.query.filter(Room.name == name).first()
@staticmethod
def get_all():
"""
Return all rooms
:return:
"""
return Room.query.all()
def __str__(self):
return self.name
def to_dict(self):
return dict(id=self.id, name=self.name)
def toJSON(self):
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
sort_keys=True, indent=4)
def pre_fill_table():
rooms = scrape_rooms()
i_tunes_u_mappings = [Room(name=room['name'], number=room['room_number'],
building_name=room['building_name'], building_number=room['building_number']) for room in
rooms]
try:
db.session.bulk_save_objects(i_tunes_u_mappings)
db.session.commit()
except IntegrityError as e:
db.session.rollback()

View File

@@ -0,0 +1,485 @@
# -*- coding: utf-8 -*-
"""
Example user model and related models
"""
import json
from sqlalchemy.orm import relation
from sqlalchemy import MetaData
from backend import db, app, login_manager
from backend.models.post_model import Post
from backend.models.example_model import ExampleDataItem
import re
import jwt
from flask_login import UserMixin
from sqlalchemy import or_, event
from datetime import datetime, timedelta
from passlib.hash import sha256_crypt
from hashlib import md5
metadata = MetaData()
followers = db.Table('followers',
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)
acquaintances = db.Table('acquaintances',
db.Column('me_id', db.Integer, db.ForeignKey('user.id')),
db.Column('acquaintance_id', db.Integer, db.ForeignKey('user.id'))
)
# This is the association table for the many-to-many relationship between
# groups and members - this is, the memberships.
user_group_table = db.Table('user_group',
db.Column('user_id', db.Integer,
db.ForeignKey('user.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
db.Column('group_id', db.Integer,
db.ForeignKey('group.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
# This is the association table for the many-to-many relationship between
# groups and permissions.
group_permission_table = db.Table('group_permission',
db.Column('group_id', db.Integer,
db.ForeignKey('group.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
db.Column('permission_id', db.Integer,
db.ForeignKey('permission.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
class User(UserMixin, db.Model):
"""
Example user model representation.
"""
id = db.Column(db.Integer, primary_key=True)
social_id = db.Column(db.Unicode(63), nullable=True, unique=True)
nickname = db.Column(db.Unicode(63), index=True, unique=True)
first_name = db.Column(db.Unicode(63), index=True, nullable=True)
last_name = db.Column(db.Unicode(63), index=True, nullable=True)
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
lang = db.Column(db.Unicode(32), index=False, unique=False)
timezone = db.Column(db.Unicode(63), index=False, unique=False)
posts = db.relationship('Post', backref='author', lazy='dynamic')
example_data_item = db.relationship('ExampleDataItem', backref='owner')
example_data_item_id = db.Column(db.ForeignKey(ExampleDataItem.id))
about_me = db.Column(db.Unicode(255))
role = db.Column(db.Unicode(63))
groups = db.relationship('Group', secondary=user_group_table, back_populates='users')
password = db.Column(db.String(255), nullable=True)
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
external_user = db.Column(db.Boolean, default=False)
last_seen = db.Column(db.DateTime, default=datetime.utcnow())
last_time_modified = db.Column(db.DateTime, default=datetime.utcnow())
jwt_exp_delta_seconds = db.Column(db.Integer, nullable=True)
acquainted = db.relationship('User',
secondary=acquaintances,
primaryjoin=(acquaintances.c.me_id == id),
secondaryjoin=(acquaintances.c.acquaintance_id == id),
backref=db.backref('acquaintances', lazy='dynamic'),
lazy='dynamic')
followed = db.relationship('User',
secondary=followers,
primaryjoin=(followers.c.follower_id == id),
secondaryjoin=(followers.c.followed_id == id),
backref=db.backref('followers', lazy='dynamic'),
lazy='dynamic')
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
password = kwargs.get("password", None)
external_user = kwargs.get("external_user", None)
if password is not None:
self.password = sha256_crypt.encrypt(password)
if external_user is not None:
self.external_user = external_user
@staticmethod
@login_manager.user_loader
def get_by_identifier(identifier):
"""
Find user by identifier, which might be the nickname or the e-mail address.
:param identifier:
:return:
"""
return User.query.filter(or_(User.nickname == identifier,
User.email == identifier,
User.id == identifier)).first()
@staticmethod
@login_manager.user_loader
def get_by_id(identifier):
"""
Find user by ID.
:param identifier:
:return:
"""
return User.query.filter(User.id == identifier).first()
@staticmethod
def get_all():
"""
Return all users
:return:
"""
return User.query.all()
@staticmethod
def make_unique_nickname(nickname):
"""
Add suffix (counter) to nickname in order to get a unique nickname.
:param nickname:
:return:
"""
if User.query.filter_by(nickname=nickname).first() is None:
return nickname
version = 2
while True:
new_nickname = nickname + str(version)
if User.query.filter_by(nickname=new_nickname).first() is None:
break
version += 1
return new_nickname
@staticmethod
def make_valid_nickname(nickname):
"""
Replaces certain characters (except a-zA-Z0-9_.) in nickname with blancs.
:param nickname:
:return:
"""
return re.sub('[^a-zA-Z0-9_.]', '', nickname)
@classmethod
def authenticate(cls, **kwargs):
email = kwargs.get('email')
password = kwargs.get('password')
if not email or not password:
return None
user = cls.query.filter_by(email=email).first()
if not user or not user.verify_password(password):
return None
return user
@property
def is_authenticated(self):
"""
Returns true if user is authenticated.
:return:
"""
# TODO: implement correctly
return True
@property
def is_active(self):
"""
Returns true if user is active.
:return:
"""
# TODO: implement correctly
return True
@property
def is_anonymous(self):
"""
Returns true if user is anonymous.
:return:
"""
# TODO: implement correctly
return False
@property
def is_read_only(self):
"""
Returns true if user is active.
:return:
"""
# TODO: implement correctly
return True
@staticmethod
def decode_auth_token(auth_token):
"""
Decodes the auth token
:param auth_token:
:return: integer|string
"""
try:
payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'))
is_blacklisted_token = BlacklistToken.check_blacklist(auth_token)
if is_blacklisted_token:
return 'Token blacklisted. Please log in again.'
else:
return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
def encode_auth_token(self):
"""
Generates the Auth Token
:return: string
"""
try:
payload = {
'exp': datetime.utcnow() + timedelta(days=0, hours=3, seconds=5),
'iat': datetime.utcnow(),
'sub': self.id
}
return jwt.encode(
payload,
app.config.get('SECRET_KEY'),
algorithm='HS256'
)
except Exception as e:
return e
def set_password(self, password):
"""
SHA256 encrypts the given password and sets it on the user.
:param password:
:return:
"""
self.password = sha256_crypt.encrypt(password)
def verify_password(self, password):
"""
Verifies that the given password matches the SHA256 encrypted password stored on the user.
:param password:
:return:
"""
if self.password is None:
return False
return sha256_crypt.verify(password, self.password)
def get_id(self):
"""
Returns the ID of the user.
:return:
"""
try:
# noinspection PyUnresolvedReferences
return unicode(self.id) # python 2
except NameError:
return str(self.id) # python 3
def avatar(self, size):
"""
Returns an avatar URL.
:param size:
:return:
"""
return 'https://s.gravatar.com/avatar/%s?d=mm&s=%d' % (md5(self.email.encode('utf-8')).hexdigest(), size)
def acquaint(self, user):
"""
Adds an acquaintance to the user object.
:param user:
:return:
"""
if not self.is_acquainted(user):
self.acquainted.append(user)
return self
def unacquaint(self, user):
"""
Removes the user from the list of acquaintances.
:param user:
:return:
"""
if self.is_acquainted(user):
self.acquainted.remove(user)
return self
def is_acquainted(self, user):
"""
Check if the provided user is an acquaintance.
:param user:
:return:
"""
return self.acquainted.filter(acquaintances.c.acquaintance_id == user.id).count() > 0
def get_acquaintances(self):
"""
Returns the list of acquaintances.
:return:
"""
return User.query.join(acquaintances, (acquaintances.c.acquaintance_id == User.id)).filter(
acquaintances.c.me_id == self.id).order_by(User.nickname.desc())
def shared_example_data_items(self):
"""
Returns a list of the shared data items.
:return:
"""
return ExampleDataItem.query.join(acquaintances,
(acquaintances.c.acquaintance_id == ExampleDataItem.user_id)).filter(
acquaintances.c.me_id == self.id).order_by(ExampleDataItem.timestamp.desc())
def follow(self, user):
"""
Add user to list of followers.
:param user:
:return:
"""
if not self.is_following(user):
self.followed.append(user)
return self
def unfollow(self, user):
"""
Remove user from the list of followers.
:param user:
:return:
"""
if self.is_following(user):
self.followed.remove(user)
return self
def is_following(self, user):
"""
Checks if specified user is a follower.
:param user:
:return:
"""
return self.followed.filter(followers.c.followed_id == user.id).count() > 0
def followed_posts(self):
"""
Returns list of followed posts.
:return:
"""
return Post.query.join(followers, (followers.c.followed_id == Post.user_id)).filter(
followers.c.follower_id == self.id).order_by(Post.timestamp.desc())
def to_dict(self):
#return self.__dict__
return dict(id=self.id, email=self.email, groups=[g.to_dict() for g in self.groups])
def toJSON(self):
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
sort_keys=True, indent=4)
def __repr__(self):
return '<User %r>' % self.email
class BlacklistToken(db.Model):
"""
Token Model for storing JWT tokens
"""
__tablename__ = 'blacklist_tokens'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
token = db.Column(db.String(500), unique=True, nullable=False)
blacklisted_on = db.Column(db.DateTime, nullable=False)
def __init__(self, token):
self.token = token
self.blacklisted_on = datetime.now()
def __repr__(self):
return '<id: token: {}'.format(self.token)
@staticmethod
def get_by_token(jwt_id):
return BlacklistToken.query.filter(BlacklistToken.token == jwt_id).first()
@staticmethod
def check_blacklist(auth_token):
"""
check whether auth token has been blacklisted
:param auth_token:
:return:
"""
res = BlacklistToken.query.filter_by(token=str(auth_token)).first()
if res:
return True
else:
return False
class Group(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
name = db.Column(db.Unicode(63), unique=True, nullable=False)
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
users = db.relationship('User', secondary=user_group_table, back_populates='groups')
permissions = db.relationship('Permission', secondary=group_permission_table, back_populates='groups')
def __init__(self, **kwargs):
super(Group, self).__init__(**kwargs)
@staticmethod
def get_by_name(name):
"""
Find group by name
:param name:
:return:
"""
return Group.query.filter(Group.name == name).first()
@staticmethod
def get_all():
"""
Return all groups
:return:
"""
return Group.query.all()
def __str__(self):
return self.name
def to_dict(self):
return dict(id=self.id, name=self.name)
def toJSON(self):
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
sort_keys=True, indent=4)
class Permission(db.Model):
"""Table containing permissions associated with groups."""
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
name = db.Column(db.Unicode(63), unique=True, nullable=False)
description = db.Column(db.Unicode(511))
groups = db.relationship(Group, secondary=group_permission_table,
back_populates='permissions')
@event.listens_for(User.__table__, 'after_create')
def insert_initial_users(*args, **kwargs):
for u in app.config.get("USERS", []):
db.session.add(User(**u))
db.session.commit()
@event.listens_for(Group.__table__, 'after_create')
def insert_initial_groups(*args, **kwargs):
for g in app.config.get("GROUPS", []):
db.session.add(Group(**g))
db.session.commit()
@event.listens_for(Permission.__table__, 'after_create')
def insert_initial_permissions(*args, **kwargs):
for p in app.config.get("PERMISSIONS", []):
db.session.add(Permission(name=p))
db.session.commit()

View File

@@ -0,0 +1,95 @@
import json
from datetime import datetime
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import backref
from backend import db
# This is the association table for the many-to-many relationship between
# virtual commands and recorder commands.
virtual_command_recorder_command_table = db.Table('virtual_command_recorder_command',
db.Column('virtual_command_id', db.Integer,
db.ForeignKey('virtual_command.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
db.Column('recorder_command_id', db.Integer,
db.ForeignKey('recorder_command.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
# This is the association table for the many-to-many relationship between
# virtual commands and recorder commands.
virtual_command_recorder_table = db.Table('virtual_command_recorder',
db.Column('virtual_command_id', db.Integer,
db.ForeignKey('virtual_command.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
db.Column('recorder_id', db.Integer,
db.ForeignKey('recorder.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
class VirtualCommand(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
name = db.Column(db.Unicode(63), unique=True, nullable=False)
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
recorders = db.relationship('Recorder', secondary=virtual_command_recorder_table,
back_populates='virtual_commands')
recorder_commands = db.relationship('RecorderCommand', secondary=virtual_command_recorder_command_table,
back_populates='virtual_commands')
# parent_virtual_command = db.relationship('VirtualCommand', back_populates='child_virtual_commands')
parent_virtual_command_id = db.Column(db.Integer, db.ForeignKey('virtual_command.id'))
child_virtual_commands = db.relationship('VirtualCommand',
backref=backref('parent_virtual_command', remote_side=[id]))
command_order_string = db.Column(db.String)
def __init__(self, **kwargs):
super(VirtualCommand, self).__init__(**kwargs)
@staticmethod
def get_by_name(name):
"""
Find group by name
:param name:
:return:
"""
return VirtualCommand.query.filter(VirtualCommand.name == name).first()
@staticmethod
def get_all():
"""
Return all groups
:return:
"""
return VirtualCommand.query.all()
@hybrid_property
def command_order(self):
if self.command_order_string is None:
return []
return json.loads(self.command_order_string)
@command_order.setter
def command_order(self, ordered_list_of_commands: list):
self.command_order_string = json.dumps(ordered_list_of_commands)
def __str__(self):
return self.name
def to_dict(self):
return dict(id=self.id, name=self.name, description=self.description)
def toJSON(self):
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
sort_keys=True, indent=4)

View File

@@ -0,0 +1,136 @@
import inspect
import pkgutil
import sys
import telnetlib
from abc import ABC, abstractmethod
# monkey patching of telnet lib
original_read_until = telnetlib.Telnet.read_until
original_write = telnetlib.Telnet.write
def new_read_until(self, match, timeout=None):
if isinstance(match, str):
return original_read_until(self, match.encode("ascii"), timeout)
else:
return original_read_until(self, match, timeout)
def new_write(self, buffer):
if isinstance(buffer, str):
return original_write(self, buffer.encode("ascii"))
else:
return original_write(self, buffer)
telnetlib.Telnet.read_until = new_read_until
telnetlib.Telnet.write = new_write
def read_line(self, timeout=2):
return self.read_until("\n", timeout)
telnetlib.Telnet.read_line = read_line
def read_until_non_empty_line(self):
line = self.read_line()
if line is None:
return None
while len(line.rstrip()) <= 0:
line = self.read_line()
return line
telnetlib.Telnet.read_until_non_empty_line = read_until_non_empty_line
def assert_string_in_output(self, string, timeout=2):
resp = self.read_until(string, timeout)
if resp is None:
return False, resp,
resp = resp.decode("ascii")
if string in resp:
return True, resp
return False, resp
telnetlib.Telnet.assert_string_in_output = assert_string_in_output
class TelnetAdapter(ABC):
def __init__(self, address, esc_char="W"):
self.address = address
self.tn = None
self.esc_char = esc_char
@abstractmethod
def _login(self):
pass
def _run_cmd(self, cmd, timeout=1, auto_connect=True):
if self.tn is None and not auto_connect:
raise Exception("Not connected!")
elif self.tn is None:
self._login()
self.tn.write(cmd)
out = self.tn.read_until_non_empty_line()
res = out
while out is not None and out != "":
out = self.tn.read_until_non_empty_line()
print(out)
res += out
return res
@staticmethod
def _get_response_str(tn_response):
if isinstance(tn_response, bytes):
return str(tn_response.decode("ascii").rstrip())
else:
return str(tn_response).rstrip()
class RecorderAdapter:
@abstractmethod
def _get_name(self):
pass
@abstractmethod
def _get_version(self):
pass
def get_defined_recorder_adapters():
models = []
found_packages = list(pkgutil.iter_modules(sys.modules[__name__].__path__))
for f_p in found_packages:
importer = f_p[0]
rec_model_module = importer.find_module(f_p[1]).load_module(f_p[1])
rec_model = {'id': f_p[1], 'name': f_p[1], 'commands': {}}
if hasattr(rec_model_module, 'RECORDER_MODEL_NAME'):
rec_model['name'] = rec_model_module.RECORDER_MODEL_NAME
if hasattr(rec_model_module, 'REQUIRES_USER'):
rec_model['requires_user'] = rec_model_module.REQUIRES_USER
if hasattr(rec_model_module, 'REQUIRES_PW'):
rec_model['requires_password'] = rec_model_module.REQUIRES_PW
for name, obj in inspect.getmembers(rec_model_module, inspect.isclass):
if issubclass(obj, RecorderAdapter):
commands = {}
for method_name, method in inspect.getmembers(obj, predicate=inspect.isfunction):
if len(method_name) > 0 and "_" == method_name[0]:
continue
signature = inspect.signature(method)
parameters = {}
for params in signature.parameters:
if params == "self":
continue
param_type = signature.parameters[params].annotation.__name__
param_type = "_unknown_type" if param_type == "_empty" else param_type
parameters[signature.parameters[params].name] = param_type
if len(parameters) <= 0:
parameters = None
commands[method_name] = parameters
rec_model["commands"] = commands
models.append(rec_model)
return models

View File

@@ -0,0 +1,786 @@
from backend.recorder_adapters import telnetlib, TelnetAdapter, RecorderAdapter
RECORDER_MODEL_NAME = "SMP 351 / 352"
VERSION = "0.9.0"
REQUIRES_USER = False
REQUIRES_PW = True
# HOST = "localhost"
# HOST = "129.13.51.102" # Audimax SMP 351
# HOST = "129.13.51.106" # Tulla SMP 351
HOST = "172.22.246.207" # Test SMP MZ
USER = "admin"
PW = "123mzsmp"
class SMP(TelnetAdapter, RecorderAdapter):
def __init__(self, address, password, **kwargs):
super().__init__(address)
self.admin_pw = password
def _login(self):
self.tn = telnetlib.Telnet(HOST)
self.tn.read_until("\r\nPassword:")
# password = getpass.getpass()
password = self.admin_pw
self.tn.write(password + "\n\r")
out = self.tn.assert_string_in_output("Login Administrator")
if not out[0]:
print(out[1])
if "Password:" in out[1]:
# TODO: loop until logged in...
print("re-enter pw")
self.tn.write("123mzsmp\n\r")
print(self.tn.assert_string_in_output("Login Administrator"))
print("WRONG (admin) password!! Exiting!")
self.tn = None
raise Exception("Could not login as administrator with given pw!")
print("OK, we have admin rights!")
def _get_name(self):
return RECORDER_MODEL_NAME
def _get_version(self):
return VERSION
def get_version(self, include_build=False, verbose_info=False):
if verbose_info:
self.tn.write("0Q")
else:
if include_build:
self.tn.write("*Q\n")
else:
self.tn.write("1Q\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_bootstrap_version(self):
self.tn.write("2Q")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_factory_firmware_version(self):
self.tn.write("3Q")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_updated_firmware_version(self):
self.tn.write("4Q")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_part_number(self):
self.tn.write("N")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_model_name(self):
self.tn.write("1I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_model_description(self):
self.tn.write("2I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_system_memory_usage(self):
self.tn.write("3I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_number_of_connected_users(self):
self.tn.write("10I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_system_processer_usage(self):
self.tn.write("11I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_system_processor_idle(self):
self.tn.write("12I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_eth0_link_status(self):
self.tn.write("13I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_file_transfer_config(self):
self.tn.write("38I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_active_alarms(self):
self.tn.write("39I")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_unit_name(self, name: str):
# TODO: check name (must comply with internet host name standards)
self.tn.write(self.esc_char + name + "CN\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_unit_name(self):
self.tn.write(self.esc_char + " CN\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_unit_name(self):
self.tn.write(self.esc_char + "CN\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_telnet_connections(self):
self.tn.write(self.esc_char + "CC\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_verbose_mode(self, mode: int):
"""
Mode:
0=clear/none (default for telnet connections
1=verbose mode (default for USB and RS-232 host control)
2=tagged responses for queries
3=verbose mode and tagged responses for queries
:param mode:
:return:
"""
if mode not in range(4):
raise Exception("Only values from 0 to 3 are allowed!")
self.tn.write(self.esc_char + str(mode) + "CV\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_verbose_mode(self):
self.tn.write(self.esc_char + "CV\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
"""
def save_configuration(self):
pass
def restore_configuration(self):
pass
"""
def reboot(self):
self.tn.write(self.esc_char + "1BOOT\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def restart_network(self):
self.tn.write(self.esc_char + "2BOOT\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_flash(self):
"""
Reset flash memory (excludes recording files).
:return:
"""
self.tn.write(self.esc_char + "ZFFF\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def system_reset(self):
"""
Resets device to default and deletes recorded files
:return:
"""
self.tn.write(self.esc_char + "ZXXX\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_settings_and_delete_all_files(self):
"""
Reset to default except IP address, delete all user and recorded files
:return:
"""
self.tn.write(self.esc_char + "ZY\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def absolute_reset(self):
"""
Same as System Reset, plus returns the IP address and subnet mask to defaults.
:return:
"""
self.tn.write(self.esc_char + "ZQQQ\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_front_panel_lock(self, mode: int):
"""
0=Off
1=complete lockout (no front panel control)
2=menu lockout
3=recording controls
:param mode: Execute mode int code
:return:
"""
if mode not in range(4):
raise Exception("Only values from 0 to 3 are allowed!")
self.tn.write(str(mode) + "X\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_front_panel_lock(self):
"""
View executive mode.
0=Off
1=complete lockout (no front panel control)
2=menu lockout
3=recording controls
:return:
"""
self.tn.write("X\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
"""
A lot of stuff related to network settings (ports of services, SNMP, IP, DHCP, etc.)
Only some stuff will be implemented here!
"""
"""
def get_date_time(self):
pass
def get_time_zone(self):
pass
def get_dhcp_mode(self):
pass
def get_network_settings(self):
pass
def get_ip_address(self):
pass
def get_mac_address(self):
pass
def get_subnet_mask(self):
pass
def get_gateway_ip(self):
pass
def get_dns_server_ip(self):
pass
"""
"""
RS-232 / serial port related stuff not implemented.
Password and security related stuff not implemented.
File related stuff not implemented. (-> use sftp)
"""
def set_input(self, input_num: int, channel_num: int):
"""
Switches input # (1 to 5) to output channel (1=A [input 1 and 2], 2=B [input 3, 4 and 5])
:param input_num:
:param channel_num:
:return:
"""
if input_num not in range(1, 6):
raise Exception("input_num must be a value between 1 and 5!")
if channel_num not in range(1, 3):
raise Exception("input_num must be a value between 1 and 2!")
self.tn.write("{}*{}!\n".format(input_num, channel_num))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input(self, channel_num: int):
if channel_num not in range(1, 2):
raise Exception("input_num must be a value between 1 and 2!")
self.tn.write("{}!\n".format(channel_num))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_input_format(self, input_num: int, input_format: int):
"""
Sets the input to the format, where the input_format parameter may be:
1 = YUVp / HDTV (default)
2 = YUVi
3 = Composite
:param input_num:
:param input_format:
:return:
"""
if input_num not in range(1, 6):
raise Exception("input_num must be a value between 1 and 5!")
if input_format not in range(1, 4):
raise Exception("input_num must be a value between 1 and 3!")
self.tn.write("{}*{}\\\n".format(input_num, input_format))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_format(self, input_num: int):
if input_num not in range(1, 6):
raise Exception("input_num must be a value between 1 and 5!")
self.tn.write("{}\\\n".format(input_num))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_input_name(self, input_num: int, input_name: str):
if input_num not in range(1, 6):
raise Exception("input_num must be a value between 1 and 5!")
if len(input_name) > 16:
raise Exception("input_name must be no longer than 16 chars")
try:
input_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("input_name must only contain ascii characters")
self.tn.write("{}{},{}NI\n".format(self.esc_char, input_num, input_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_name(self, input_num: int):
if input_num not in range(1, 6):
raise Exception("input_num must be a value between 1 and 5!")
self.tn.write("{}{}NI\n".format(self.esc_char, input_num))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_selction_per_channel(self):
self.tn.write("32I\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
"""
Input configuration part skipped
"""
def stop_recording(self):
self.tn.write("{}Y0RCDR\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def start_recording(self):
self.tn.write("{}Y1RCDR\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def pause_recording(self):
self.tn.write("{}Y2RCDR\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_recording_status(self):
"""
Status may be one of:
0=stop
1=record
2=pause
:return: status
"""
self.tn.write("{}YRCDR\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def extent_recording_time(self, extension_time: int):
"""
Extends a scheduled recording by extension_time minutes
:param extension_time: must be an int from 0 to 99
:return:
"""
if extension_time not in range(0, 100):
raise Exception("extension_time must be a value between 0 and 99!")
self.tn.write("{}E{}RCDR\n".format(self.esc_char, extension_time))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def add_chapter_marker(self):
self.tn.write("{}BRCDR\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def swap_channel_positions(self):
self.tn.write("%\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_recording_status_text(self):
self.tn.write("I\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_elapsed_recording_time(self):
self.tn.write("35I\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_remaining_recording_time(self):
self.tn.write("36I\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_recording_destination(self):
self.tn.write("37I\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
"""
Metadata part skipped
"""
def recall_user_preset(self, channel_number: int, preset_number: int):
if channel_number not in range(1, 3):
raise Exception("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("1*{}*{}.\n".format(channel_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def save_user_preset(self, channel_number: int, preset_number: int):
if channel_number not in range(1, 3):
raise Exception("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("1*{}*{},\n".format(channel_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_user_preset_name(self, preset_number: int, preset_name: str):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise Exception("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("preset_name must only contain ascii characters")
self.tn.write("{}1*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_user_preset_name(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}1*{}PNAM\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_user_presets(self, input_number: int):
if input_number not in range(1, 6):
raise Exception("input_number must be a value between 1 and 5!")
self.tn.write("52*{}#\n".format(input_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
# Input Presets
def recall_input_preset(self, channel_number: int, preset_number: int):
if channel_number not in range(1, 3):
raise Exception("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 129):
raise Exception("preset_number must be a value between 1 and 128!")
self.tn.write("2*{}*{}.\n".format(channel_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def save_input_preset(self, channel_number: int, preset_number: int):
if channel_number not in range(1, 3):
raise Exception("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 129):
raise Exception("preset_number must be a value between 1 and 128!")
self.tn.write("1*{}*{},\n".format(channel_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_input_preset_name(self, preset_number: int, preset_name: str):
if preset_number not in range(1, 129):
raise Exception("preset_number must be a value between 1 and 128!")
if len(preset_name) > 16:
raise Exception("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("preset_name must only contain ascii characters")
self.tn.write("{}2*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_preset_name(self, preset_number: int):
if preset_number not in range(1, 129):
raise Exception("preset_number must be a value between 1 and 128!")
self.tn.write("{}2*{}PNAM\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def delete_input_preset(self, preset_number: int):
if preset_number not in range(1, 129):
raise Exception("preset_number must be a value between 1 and 128!")
self.tn.write("{}X2*{}PRST\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_presets(self):
self.tn.write("51#\n")
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
# Streaming Presets
def recall_streaming_preset(self, output_number: int, preset_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
3 = Confidence Stream
:param preset_number:
:param output_number:
:return:
"""
if output_number not in range(1, 4):
raise Exception("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("3*{}*{}.\n".format(output_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def save_streaming_preset(self, output_number: int, preset_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
3 = Confidence Stream
:param output_number:
:param preset_number:
:return:
"""
if output_number not in range(1, 4):
raise Exception("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("3*{}*{},\n".format(output_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_streaming_preset_name(self, preset_number: int, preset_name: str):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise Exception("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("preset_name must only contain ascii characters")
self.tn.write("{}3*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_streaming_preset_name(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}3*{}PNAM\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_streaming_preset_to_default(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}X3*{}PRST\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
# Encoder Presets
def recall_encoder_preset(self, output_number: int, preset_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
3 = Confidence Stream
:param preset_number:
:param output_number:
:return:
"""
if output_number not in range(1, 4):
raise Exception("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("4*{}*{}.\n".format(output_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def save_encoder_preset(self, output_number: int, preset_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
3 = Confidence Stream
:param preset_number:
:param output_number:
:return:
"""
if output_number not in range(1, 4):
raise Exception("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("4*{}*{},\n".format(output_number, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_encoder_preset_name(self, preset_number: int, preset_name: str):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise Exception("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("preset_name must only contain ascii characters")
self.tn.write("{}4*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_encoder_preset_name(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}4*{}PNAM\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_encoder_preset_to_default(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}X4*{}PRST\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
# Layout Presets
def save_layout_preset(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("7*{},\n".format(preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def recall_layout_preset(self, preset_number: int, include_input_selections: bool = True):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
if include_input_selections:
self.tn.write("7*{}.\n".format(preset_number))
else:
self.tn.write("8*{}.\n".format(preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_layout_preset_name(self, preset_number: int, preset_name: str):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise Exception("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise Exception("preset_name must only contain ascii characters")
self.tn.write("{}7*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_layout_preset_name(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}7*{}PNAM\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def reset_layout_preset_to_default(self, preset_number: int):
if preset_number not in range(1, 17):
raise Exception("preset_number must be a value between 1 and 16!")
self.tn.write("{}X7*{}PRST\n".format(self.esc_char, preset_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
"""
Input adjustments skipped
Picture adjustments skipped
"""
def mute_output(self, output_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
:param output_number:
:return:
"""
if output_number not in range(1, 3):
raise Exception("output_number must be a value between 1 and 2!")
self.tn.write("{}*1B\n".format(output_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def unmute_output(self, output_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
:param output_number:
:return:
"""
if output_number not in range(1, 3):
raise Exception("output_number must be a value between 1 and 2!")
self.tn.write("{}*0B\n".format(output_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def is_muted(self, output_number: int):
"""
Output_number:
1 = Channel A
2 = Channel B
:param output_number:
:return:
"""
if output_number not in range(1, 3):
raise Exception("output_number must be a value between 1 and 2!")
self.tn.write("{}B\n".format(output_number))
return int(TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())) > 0
"""
EDID skipped
Encoder settings skipped
some advanced options skipped
"""
def get_input_hdcp_status(self, input_number: int):
"""
returns:
0 = no sink / source detected
1 = sink / source detected with HDCP
2 = sink / source detected without HDCP
:param input_number: from 1 to 5
:return:
"""
if input_number not in range(1, 6):
raise Exception("input_number must be a value between 1 and 6!")
self.tn.write("{}I{}HDCP\n".format(self.esc_char, input_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_input_authorization_hdcp_on(self, input_number: int):
if input_number not in range(1, 6):
raise Exception("input_number must be a value between 1 and 6!")
self.tn.write("{}E{}*1HDCP\n".format(self.esc_char, input_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def set_input_authorization_hdcp_off(self, input_number: int):
if input_number not in range(1, 6):
raise Exception("input_number must be a value between 1 and 6!")
self.tn.write("{}E{}*0HDCP\n".format(self.esc_char, input_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_input_authorization_hdcp_status(self, input_number: int):
if input_number not in range(1, 6):
raise Exception("input_number must be a value between 1 and 6!")
self.tn.write("{}E{}HDCP\n".format(self.esc_char, input_number))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def enable_hdcp_notification(self):
self.tn.write("{}N1HDCP\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def disable_hdcp_notification(self):
self.tn.write("{}N0HDCP\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_hdcp_notification_status(self):
self.tn.write("{}NHDCP\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
# background image settings
def set_background_image(self, filename: str):
self.tn.write("{}{}RF\n".format(self.esc_char, filename))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def get_background_image_filename(self):
self.tn.write("{}RF\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def mute_background_image(self):
self.tn.write("{}0RF\n".format(self.esc_char))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def main():
smp = SMP(HOST, PW)
print(smp)
smp._login()
print(smp.get_version(verbose_info=False))
print(smp.get_bootstrap_version())
print(smp.get_part_number())
print(smp.get_model_name())
print(smp.get_model_description())
print(smp.get_system_memory_usage())
print(smp.get_file_transfer_config())
# print(smp.get_unit_name())
# print(smp.set_unit_name("mzsmp"))
# print(smp.get_unit_name())
# print(smp.reset_unit_name())
print(smp.set_front_panel_lock(0))
print(smp.get_front_panel_lock())
print(smp.get_input_name(1))
print(smp.get_input_selction_per_channel())
print(smp.get_recording_status())
print("Preset Name: " + smp.get_user_preset_name(2))
print(smp.get_user_presets(1))
print(smp.get_input_presets())
print(smp.get_layout_preset_name(2))
print(smp.get_encoder_preset_name(1))
print(smp.get_streaming_preset_name(2))
print(smp.recall_encoder_preset(3, 1))
print(smp.is_muted(2))
print(smp.mute_output(2))
print(smp.is_muted(2))
print(smp.unmute_output(2))
print(smp.is_muted(2))
if __name__ == '__main__':
main()

76
backend/serve_frontend.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
from flask.json import dump
from jose import jwt, jwk
import os
from flask import render_template, send_from_directory, Blueprint, jsonify, url_for
from flask_pyoidc.user_session import UserSession
from backend import app
from backend.auth import oidc_auth
fe_path = os.path.abspath(os.path.join(app.root_path, os.pardir, os.pardir, "frontend", "dist"))
if not os.path.exists(fe_path) or not os.path.exists(os.path.join(fe_path, "index.html")):
app.logger.critical(
"Frontend path and/or index.html does not exist! Please build frontend before continuing! "
"You might want to go to ../frontend and continue from there.")
exit()
fe_bp = Blueprint('frontend', __name__, url_prefix='/', template_folder=os.path.join(fe_path, ""))
@fe_bp.route('/js/<path:path>')
def send_js(path):
return send_from_directory(os.path.join(fe_path, "js"), path)
@fe_bp.route('/css/<path:path>')
def send_css(path):
return send_from_directory(os.path.join(fe_path, "css"), path)
@fe_bp.route('/img/<path:path>')
def send_img(path):
return send_from_directory(os.path.join(fe_path, "img"), path)
@fe_bp.route('/test')
@oidc_auth.oidc_auth()
def test_oidc():
user_session = UserSession(flask.session)
access_token = user_session.access_token
token_claim = jwt.get_unverified_claims(access_token)
token_header = jwt.get_unverified_header(access_token)
return jsonify(id_token=flask.session['id_token'], access_token=flask.session['access_token'],
userinfo=flask.session['userinfo'],
token_claim=token_claim,
token_header=token_header)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
@fe_bp.route("/site-map")
def site_map():
print("# serving site-map!!")
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if has_no_empty_params(rule):
# if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
# links is now a list of url, endpoint tuples
# dump(links)
return jsonify(links)
@fe_bp.route('/', defaults={'path': ''})
@fe_bp.route('/<path:path>')
def catch_all(path):
return render_template("index.html")

View File

20
backend/tests/base.py Normal file
View File

@@ -0,0 +1,20 @@
from flask_testing import TestCase
from backend import app, db
class BaseTestCase(TestCase):
""" Base Tests """
def create_app(self):
app.config.from_object('backend.config.Config')
return app
def setUp(self):
db.create_all()
db.session.commit()
def tearDown(self):
db.session.remove()
db.drop_all()

View File

@@ -0,0 +1,65 @@
import getpass
import sys
from abc import ABC, abstractmethod
from backend.recorder_adapters import telnetlib
# HOST = "localhost"
# HOST = "129.13.51.102" # Audimax SMP 351
# HOST = "129.13.51.106" # Tulla SMP 351
HOST = "172.22.246.207" # Test SMP MZ
user = "admin"
pw = "123mzsmp"
def print_tn(tn_response):
if isinstance(tn_response, bytes):
print(tn_response.decode("ascii").rstrip())
else:
print(str(tn_response).rstrip())
def run_cmd(tn, cmd, timeout=1):
tn.write(cmd)
out = tn.read_until_non_empty_line()
res = out
while out is not None and out != "":
out = tn.read_until_non_empty_line()
print(out)
res += out
return res
tn = telnetlib.Telnet(HOST)
tn.read_until("\r\nPassword:")
# password = getpass.getpass()
password = pw
tn.write(password + "\n\r")
if not tn.assert_string_in_output("Login Administrator")[0]:
print("WRONG (admin) password!! Exiting!")
exit(1)
print("OK, we have admin rights!")
tn.write("1Q\n")
# print_tn(read_line(tn))
print_tn(tn.read_until_non_empty_line())
print("test")
# print(run_cmd(tn, "I\n"))
# run_cmd(tn, "X1CERT\n")
# tn.write(chr(27)+"X1CERT")
# tn.write("WX1CERT\n")
# tn.write(chr(27)+"1BOOT")
# tn.write("W1BOOT\n")
# print_tn(tn.read_until_non_empty_line())
# tn.write("I\n")
# print_tn(tn.read_some())
# tn.write("exit\n\r".encode("ascii"))
# print_tn(tn.read_eager())

View File

@@ -0,0 +1,49 @@
import os
import unittest
from flask import current_app
from flask_testing import TestCase
from backend import app
basedir = os.path.abspath(os.path.join(os.path.abspath(app.root_path), os.pardir))
class TestDevelopmentConfig(TestCase):
def create_app(self):
app.config.from_object('backend.config.DevelopmentConfig')
return app
def test_app_is_development(self):
self.assertFalse(app.config['SECRET_KEY'] is 'you-will-never-guess')
self.assertTrue(app.config['DEBUG'] is True)
self.assertFalse(current_app is None)
self.assertTrue(
app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'app.db_debug')
)
class TestTestingConfig(TestCase):
def create_app(self):
app.config.from_object('backend.config.TestingConfig')
return app
def test_app_is_testing(self):
self.assertFalse(app.config['SECRET_KEY'] is 'you-will-never-guess')
self.assertTrue(app.config['DEBUG'])
self.assertTrue(
app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'app.db_test')
)
class TestProductionConfig(TestCase):
def create_app(self):
app.config.from_object('backend.config.Config')
return app
def test_app_is_production(self):
self.assertTrue(app.config['DEBUG'] is False)
if __name__ == '__main__':
unittest.main()

287
backend/tests/test_auth.py Normal file
View File

@@ -0,0 +1,287 @@
import unittest
import json
import time
from backend import db
from backend.models.user_model import User, BlacklistToken
from backend.tests.base import BaseTestCase
def register_user(self, email, password):
return self.client.post(
'/auth/register',
data=json.dumps(dict(
email=email,
password=password
)),
content_type='application/json',
)
class TestAuthBlueprint(BaseTestCase):
def test_registration(self):
""" Test for user registration """
with self.client:
response = register_user(self, 'joe@gmail.com', '123456')
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'success')
self.assertTrue(data['message'] == 'Successfully registered.')
self.assertTrue(data['auth_token'])
self.assertTrue(response.content_type == 'application/json')
self.assertEqual(response.status_code, 201)
def test_registered_with_already_registered_user(self):
""" Test registration with already registered email"""
user = User(email='joe@gmail.com', password='test')
db.session.add(user)
db.session.commit()
with self.client:
response = register_user(self, 'joe@gmail.com', '123456')
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(
data['message'] == 'User already exists. Please Log in.')
self.assertTrue(response.content_type == 'application/json')
self.assertEqual(response.status_code, 202)
def test_registered_user_login(self):
""" Test for login of registered-user login """
with self.client:
# user registration
resp_register = register_user(self, 'joe@gmail.com', '123456')
data_register = json.loads(resp_register.data.decode())
self.assertTrue(data_register['status'] == 'success')
self.assertTrue(
data_register['message'] == 'Successfully registered.'
)
self.assertTrue(data_register['auth_token'])
self.assertTrue(resp_register.content_type == 'application/json')
self.assertEqual(resp_register.status_code, 201)
# registered user login
response = self.client.post(
'/auth/login',
data=json.dumps(dict(
nickname='test_nick',
email='joe@gmail.com',
password='123456'
)),
content_type='application/json'
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'success')
self.assertTrue(data['message'] == 'Successfully logged in.')
self.assertTrue(data['auth_token'])
self.assertTrue(response.content_type == 'application/json')
self.assertEqual(response.status_code, 200)
def test_non_registered_user_login(self):
""" Test for login of non-registered user """
with self.client:
response = self.client.post(
'/auth/login',
data=json.dumps(dict(
nickname='test_nick',
email='joe@gmail.com',
password='123456'
)),
content_type='application/json'
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(data['message'] == 'User does not exist.')
self.assertTrue(response.content_type == 'application/json')
self.assertEqual(response.status_code, 404)
def test_user_status(self):
""" Test for user status """
with self.client:
resp_register = register_user(self, 'joe@gmail.com', '123456')
response = self.client.get(
'/auth/status',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_register.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'success')
self.assertTrue(data['data'] is not None)
self.assertTrue(data['data']['email'] == 'joe@gmail.com')
# self.assertTrue(data['data']['admin'] is 'true' or 'false')
self.assertEqual(response.status_code, 200)
def test_valid_logout(self):
""" Test for logout before token expires """
with self.client:
# user registration
resp_register = register_user(self, 'joe@gmail.com', '123456')
data_register = json.loads(resp_register.data.decode())
self.assertTrue(data_register['status'] == 'success')
self.assertTrue(
data_register['message'] == 'Successfully registered.')
self.assertTrue(data_register['auth_token'])
self.assertTrue(resp_register.content_type == 'application/json')
self.assertEqual(resp_register.status_code, 201)
# user login
resp_login = self.client.post(
'/auth/login',
data=json.dumps(dict(
email='joe@gmail.com',
password='123456'
)),
content_type='application/json'
)
data_login = json.loads(resp_login.data.decode())
self.assertTrue(data_login['status'] == 'success')
self.assertTrue(data_login['message'] == 'Successfully logged in.')
self.assertTrue(data_login['auth_token'])
self.assertTrue(resp_login.content_type == 'application/json')
self.assertEqual(resp_login.status_code, 200)
# valid token logout
response = self.client.post(
'/auth/logout',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_login.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'success')
self.assertTrue(data['message'] == 'Successfully logged out.')
self.assertEqual(response.status_code, 200)
def test_invalid_logout(self):
""" Testing logout after the token expires """
with self.client:
# user registration
resp_register = register_user(self, 'joe@gmail.com', '123456')
data_register = json.loads(resp_register.data.decode())
self.assertTrue(data_register['status'] == 'success')
self.assertTrue(
data_register['message'] == 'Successfully registered.')
self.assertTrue(data_register['auth_token'])
self.assertTrue(resp_register.content_type == 'application/json')
self.assertEqual(resp_register.status_code, 201)
# user login
resp_login = self.client.post(
'/auth/login',
data=json.dumps(dict(
email='joe@gmail.com',
password='123456'
)),
content_type='application/json'
)
data_login = json.loads(resp_login.data.decode())
self.assertTrue(data_login['status'] == 'success')
self.assertTrue(data_login['message'] == 'Successfully logged in.')
self.assertTrue(data_login['auth_token'])
self.assertTrue(resp_login.content_type == 'application/json')
self.assertEqual(resp_login.status_code, 200)
# invalid token logout
time.sleep(6)
response = self.client.post(
'/auth/logout',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_login.data.decode()
)['auth_token']
)
)
print(response.data)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(
data['message'] == 'Signature expired. Please log in again.')
self.assertEqual(response.status_code, 401)
def test_valid_blacklisted_token_logout(self):
""" Test for logout after a valid token gets blacklisted """
with self.client:
# user registration
resp_register = register_user(self, 'joe@gmail.com', '123456')
data_register = json.loads(resp_register.data.decode())
self.assertTrue(data_register['status'] == 'success')
self.assertTrue(
data_register['message'] == 'Successfully registered.')
self.assertTrue(data_register['auth_token'])
self.assertTrue(resp_register.content_type == 'application/json')
self.assertEqual(resp_register.status_code, 201)
# user login
resp_login = self.client.post(
'/auth/login',
data=json.dumps(dict(
email='joe@gmail.com',
password='123456'
)),
content_type='application/json'
)
data_login = json.loads(resp_login.data.decode())
self.assertTrue(data_login['status'] == 'success')
self.assertTrue(data_login['message'] == 'Successfully logged in.')
self.assertTrue(data_login['auth_token'])
self.assertTrue(resp_login.content_type == 'application/json')
self.assertEqual(resp_login.status_code, 200)
# blacklist a valid token
blacklist_token = BlacklistToken(
token=json.loads(resp_login.data.decode())['auth_token'])
db.session.add(blacklist_token)
db.session.commit()
# blacklisted valid token logout
response = self.client.post(
'/auth/logout',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_login.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(data['message'] == 'Token blacklisted. Please log in again.')
self.assertEqual(response.status_code, 401)
def test_valid_blacklisted_token_user(self):
""" Test for user status with a blacklisted valid token """
with self.client:
resp_register = register_user(self, 'joe@gmail.com', '123456')
# blacklist a valid token
blacklist_token = BlacklistToken(
token=json.loads(resp_register.data.decode())['auth_token'])
db.session.add(blacklist_token)
db.session.commit()
response = self.client.get(
'/auth/status',
headers=dict(
Authorization='Bearer ' + json.loads(
resp_register.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(data['message'] == 'Token blacklisted. Please log in again.')
self.assertEqual(response.status_code, 401)
def test_user_status_malformed_bearer_token(self):
""" Test for user status with malformed bearer token"""
with self.client:
resp_register = register_user(self, 'joe@gmail.com', '123456')
response = self.client.get(
'/auth/status',
headers=dict(
Authorization='Bearer' + json.loads(
resp_register.data.decode()
)['auth_token']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'fail')
self.assertTrue(data['message'] == 'Bearer token malformed.')
self.assertEqual(response.status_code, 401)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,36 @@
import unittest
from backend import db
from backend.models.user_model import User
from backend.tests.base import BaseTestCase
class TestUserModel(BaseTestCase):
def test_encode_auth_token(self):
user = User(
nickname='testheini',
email='test@test.com',
password='test'
)
db.session.add(user)
db.session.commit()
auth_token = user.encode_auth_token()
self.assertTrue(isinstance(auth_token, bytes))
def test_decode_auth_token(self):
user = User(
nickname='testheini',
email='test@test.com',
password='test'
)
db.session.add(user)
db.session.commit()
auth_token = user.encode_auth_token()
self.assertTrue(isinstance(auth_token, bytes))
self.assertTrue(User.decode_auth_token(
auth_token.decode("utf-8")) == 1)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,2 @@
# Copyright (c) 2019. Tobias Kurze, KIT

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2019. Tobias Kurze
import hashlib
import logging
from datetime import datetime
from json import dumps
from pprint import pprint
from sqlalchemy import and_
from backend import db
from backend.models.recorder_model import RecorderModel, RecorderCommand
from backend.recorder_adapters import get_defined_recorder_adapters
def calculate_md5_checksum(string_to_md5_sum: str):
return hashlib.md5(string_to_md5_sum.encode('utf-8')).hexdigest()
def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel):
existing_recorder_commands = RecorderCommand.query.filter(
and_(RecorderCommand.name.in_(command_definitions.keys())),
RecorderCommand.recorder_model == recorder_model)
existing_commands = set()
for existing_command in existing_recorder_commands:
existing_commands.add(existing_command.name)
args = command_definitions.get(existing_command.name)
if dumps(existing_command.parameters) != dumps(args):
logging.warning(
"The function definition {} collides with an existing definition of the same name "
"but different parameters!".format(
existing_command.name))
existing_command.last_time_modified = datetime.utcnow()
existing_command.parameters = args
db.session.commit()
for c_d in set(command_definitions.keys()) - existing_commands:
# print(c_d)
args = command_definitions.get(c_d)
# create new recorder command(s)
r_c = RecorderCommand(name=c_d, parameters=args, recorder_model=recorder_model)
db.session.add(r_c)
db.session.commit()
def update_recorder_models_database():
r_as = get_defined_recorder_adapters()
for r_a in r_as:
r_m = RecorderModel.get_by_adapter_id(r_a["id"])
model_checksum = calculate_md5_checksum(dumps(r_a["commands"]))
if r_m is None:
r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum,
requires_user=r_a.get('requires_user', None),
requires_password=r_a.get('requires_password', None))
db.session.add(r_m)
db.session.flush()
db.session.refresh(r_m)
else:
if not model_checksum == r_m.checksum:
r_m.model_name = r_a["name"]
r_m.model_name = r_a["name"]
r_m.checksum = model_checksum
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
db.session.commit()
if __name__ == '__main__':
db.create_all()
update_recorder_models_database()

View File

@@ -0,0 +1,60 @@
from pprint import pprint
import re
import requests
from bs4 import BeautifulSoup
def scrape_rooms():
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
room_url = "https://campus.kit.edu/live-stud/campus/all/roomgroup.asp?roomgroupcolumn1=H%F6r%2D%2FLehrsaal&tguid=0x1A35C3A1490748388EBEBA3943EFCDD5"
page = requests.get(room_url, headers=headers)
# soup = BeautifulSoup(page.content, 'html5lib')
soup = BeautifulSoup(page.content, 'html.parser')
# pprint(page.content)
# pprint(soup.prettify())
idx = 0
rooms = []
re_string = r"^(\d\d.\d\d)?\s(.*)"
re_exp = re.compile(re_string)
for tr in soup.find_all('tr'):
idx += 1
if idx == 1: # skip first row
continue
a_name = tr.find_all('a')[0].string
a_building = tr.find_all('a')[3].string
match = re_exp.match(a_name)
if match is not None:
building_number, name = re_exp.match(a_name).groups()
else:
name = a_name
building_number = None
match = re_exp.match(a_building)
if match is not None:
building_number, building_name = re_exp.match(a_building).groups()
else:
building_name = a_name
building_number = None
room = {'name': name,
'room_number': tr.find_all('a')[1].string if tr.find_all('a')[0].string != "None" else tr.find_all('a')[
1].string,
'building_name': building_name,
'building_number': building_number}
rooms.append(room)
return rooms
if __name__ == '__main__':
scrape_rooms()