71 Commits

Author SHA1 Message Date
Tobias Kurze
419056bf03 using python 3.6 (as requirement as well on production) 2020-03-12 11:38:19 +01:00
tobias.kurze
26251ed8e7 Add README.md 2020-03-12 11:28:39 +01:00
f181e4a785 further advanced state api and stream checks 2020-02-25 17:00:01 +01:00
1d011af64b added state api 2020-02-24 16:00:41 +01:00
6ba38cd42d removed flask-restplus and replaced with flask-restx 2020-02-24 09:26:47 +01:00
1745f56ac7 added stream sanity checks (sound, singe color) 2020-02-21 17:10:14 +01:00
Tobias Kurze
6971b4b618 added stream checks 2020-02-21 07:22:26 +01:00
bb4db25dcd code to call recorder functions from frointend 2019-12-12 18:44:07 +01:00
Tobias Kurze
da200f95b8 working websocket communication for recorder states 2019-12-11 08:31:42 +01:00
Tobias Kurze
190f728eb7 changed a lot regarding rec state, etc. 2019-12-03 16:05:02 +01:00
a709dbcaef added permissions api and websocket stuff 2019-11-28 19:39:53 +01:00
c4b54357f7 automatically adding recorders, models and rooms and association of those 2019-11-22 14:26:16 +01:00
Tobias Kurze
7700b4381f now rooms, recorders and recorder models get initialized in DB 2019-11-20 16:19:59 +01:00
Tobias Kurze
60ff5bdeaf added code to initialize db with models and recodres 2019-11-20 12:41:05 +01:00
bc1347fe99 changed pw in recorders.jso 2019-11-14 15:40:57 +01:00
9ef9d98132 added logging to smp adapter 2019-11-14 15:19:44 +01:00
bab6403f91 changed thread pool code again 2019-11-14 15:08:06 +01:00
692649e08f changed thread pool code again 2019-11-14 15:05:06 +01:00
6f1e01781a changed thread pool code again 2019-11-14 15:02:20 +01:00
7224038e64 modified threadpool run to use timeout 2019-11-14 14:56:55 +01:00
c96cdfc6e1 fixed bug in smp adapter 2019-11-14 14:39:27 +01:00
c3d8a1e91a fixed bug in smp adapter 2019-11-14 14:29:12 +01:00
b214d161bf slightly modified smp adapter for testing 2019-11-14 14:23:19 +01:00
e19ce060d8 exception decorator and mail send for errors and changes to rec state checker 2019-11-14 14:15:36 +01:00
Tobias Kurze
6081486a35 using git-crypt instead of manual encrypton filters 2019-11-13 13:04:18 +01:00
Tobias Kurze
2da856dd36 using git-crypt instead of manual encrypton filters 2019-11-13 12:59:09 +01:00
Tobias Kurze
872e531ef5 added logging conf and now checking recording status with simple script ; notifications are missing 2019-11-13 08:19:39 +01:00
Tobias Kurze
4563e16137 moved creds to config 2019-11-12 12:43:45 +01:00
Tobias Kurze
8b05bb694f added excel->json recorder source generation script 2019-11-12 12:32:49 +01:00
5d731c9fba moved some models added cron and websocket base class 2019-10-31 16:13:02 +01:00
Tobias Kurze
36c889956f added websocket test flies 2019-10-30 11:54:53 +01:00
Tobias Kurze
a408fd3b4c added ws base code 2019-10-25 15:59:41 +02:00
Tobias Kurze
32c2674210 added code for saving config (might not be complete 2019-10-25 13:49:41 +02:00
d61c395d2c added access control model and more stuff around access mgmt 2019-10-23 16:33:24 +02:00
6b4f7c8118 moved everything to a new module called backend 2019-10-23 15:00:33 +02:00
310d5f4820 using current user in some placesi 2019-08-15 14:32:04 +02:00
9ab0d43f43 profile and other stuff 2019-08-14 16:38:03 +02:00
Tobias Kurze
859a5d880a working on control 2019-08-13 16:30:45 +02:00
Tobias Kurze
f70cbdc463 now scraping rooms from capmus mgmt 2019-08-13 15:29:37 +02:00
48505b76ea fixed (circumvented) bug related to hybrid_property and parameter serialization 2019-08-08 20:31:08 +02:00
c0e56cf40d now scanning model difinietons and updating db entries 2019-08-08 16:38:15 +02:00
Tobias Kurze
123eb65f8e some changes to virtual command 2019-08-05 15:28:39 +02:00
Tobias Kurze
186614bc4a added virtual commands model 2019-08-05 13:41:12 +02:00
Tobias Kurze
51536766bf using reflection to get models 2019-08-02 16:09:39 +02:00
Tobias Kurze
3c6b6ba099 finished smp extron telnet adapter (more or less) 2019-07-04 13:29:44 +02:00
4d01f7025b added more smp >API< functions 2019-07-03 16:09:10 +02:00
Tobias Kurze
3a7d8dbdd4 just added comment for TODO 2019-07-03 13:51:51 +02:00
Tobias Kurze
88f5c3023d added lot of functs for SMP 2019-06-21 16:32:08 +02:00
Tobias Kurze
4485dea583 added recorder command backend 2019-06-21 11:04:12 +02:00
70df74cecf updated recorder model 2019-05-03 18:40:43 +02:00
Tobias Kurze
fad2238b75 added ports to api model 2019-05-03 15:52:52 +02:00
Tobias Kurze
295aadfaeb fixed and extended apis 2019-04-26 15:34:23 +02:00
07d01304be added room and recorder api and added length check (for sqlite) 2019-04-26 09:42:52 +02:00
Tobias Kurze
254637bfa9 some migrations; commit just to be on 'nice' state 2019-04-17 09:07:24 +02:00
3ecc8e0955 some changes to auth and group api 2019-04-15 14:14:42 +02:00
cbc269edf2 removed old dB code and continued on group and user mgmt 2019-04-11 16:18:46 +02:00
Tobias Kurze
2451a56403 added / implemented group API 2019-04-10 15:11:21 +02:00
Tobias Kurze
f0783d97c8 changes to auth and user API 2019-04-05 16:21:24 +02:00
8b7b2f489c added user and group API and models 2019-04-04 16:05:36 +02:00
cfa12717e0 added toJSON func 2019-04-02 16:58:59 +02:00
Tobias Kurze
8cf8632c8c added db migrations and group support, still problems with json serialization 2019-04-02 16:10:46 +02:00
024f063bea better separation between api and frontend login 2019-04-02 10:47:53 +02:00
Tobias Kurze
ed57dc2720 creating users from oidc id token 2019-03-26 16:17:18 +01:00
Tobias Kurze
cad27733f0 oicd now working 2019-03-26 14:58:37 +01:00
Tobias Kurze
bc50e23a22 improved/fixed serve_frontend.py to work from different base paths (change required for wsgi) 2019-03-26 11:57:22 +01:00
Tobias Kurze
ebc34e396d slightly changed user model (sort of fix) 2019-03-22 16:38:42 +01:00
0469b8dbb5 added a lot of auth code 2019-03-21 16:17:25 +01:00
Tobias Kurze
bef3c6dc9b added authentication API 2019-03-19 16:34:05 +01:00
Tobias Kurze
9d9c58d268 added OpenID Connect support 2019-03-19 15:29:26 +01:00
bd9b6c61d3 working db and tests Nr2 2019-03-14 17:14:05 +01:00
1c8cb55b46 working db and tests 2019-03-14 17:13:02 +01:00
78 changed files with 7407 additions and 495 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
PYTHONPATH=${PYTHONPATH}:${PWD}/..

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
backend/models/initial_recorders.json filter=git-crypt diff=git-crypt
#backend/models/initial_recorders.json filter=openssl diff=openssl
backend/config.py filter=git-crypt diff=git-crypt
#backend/config.py filter=openssl diff=openssl
[merge]
renormalize=true

120
.gitignore vendored
View File

@@ -1 +1,121 @@
app.db
# Byte-compiled / optimized / DLL files
.idea
__pycache__/
node_modules/
frontend/node_modules/
backend/uploads/*
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
package-lock.json
logs

32
Pipfile
View File

@@ -4,15 +4,43 @@ verify_ssl = true
name = "pypi"
[packages]
ffmpeg-python = "*"
flask = "*"
flask-httpauth = "*"
flask-restplus-patched = "*"
flask-sqlalchemy = "*"
flask-login = "*"
pyjwt = "*"
passlib = "*"
sqlalchemy = "*"
sqlalchemy-migrate = "*"
flask-script = "*"
flask-migrate = "*"
coverage = "*"
flask-testing = "*"
flask-pyoidc = "*"
python-jose = "*"
flask-jwt-extended = "*"
ssh2-python = "*"
update = "*"
flask-cors = "*"
html5lib = "*"
beautifulsoup4 = "*"
flask-socketio = "*"
eventlet = "*"
ics = "*"
coloredlogs = "*"
pythonping = "*"
scapy = "*"
python-socketio = {version = "*",extras = ["client"]}
socketio-client = "*"
websocket-client = "*"
apscheduler = "*"
pillow = "*"
pydub = "*"
simpleaudio = "*"
flask-restx = "*"
[dev-packages]
[requires]
python_version = "3.7"
python_version = "3.6"

778
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "196d1a010314c8f16ccc747ed35b4821e8e3aa63f90ec2893123ea19c95935b1"
"sha256": "ed9faf5db1ecd9f6524b4e3903b2d2e3cfedff03a928c6921dd799bb1e4ed90b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
"python_version": "3.6"
},
"sources": [
{
@@ -16,85 +16,393 @@
]
},
"default": {
"alembic": {
"hashes": [
"sha256:791a5686953c4b366d3228c5377196db2f534475bb38d26f70eb69668efd9028"
],
"version": "==1.4.1"
},
"aniso8601": {
"hashes": [
"sha256:29ad6be3828ab6ac2a31fd2876fd84477cde11890ffca7e8a9434aad5d4acec8",
"sha256:a5c7595bb65d3919a9944a759d907b57c4d050abaa0e5cf845e84c26cdfd1218"
"sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072",
"sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a"
],
"version": "==5.1.0"
"version": "==8.0.0"
},
"apispec": {
"apscheduler": {
"hashes": [
"sha256:57a7b81fd19fff0663a7e5ffd196eaea79b5364151ed2b65533be36d55e0229c",
"sha256:b45def53903516e67e8584ee41f34bc60c3e4acace6892b69340293ea20f3caa"
"sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244",
"sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"
],
"version": "==1.0.0"
"index": "pypi",
"version": "==3.6.3"
},
"arrow": {
"hashes": [
"sha256:4bfacea734ead51495dc47df00421ecfd4ca1f2c0fbe58b9a26eaeddedc31caf",
"sha256:67f8be7c0cf420424bc62d8d7dc40b44e4bb2f7b515f9cc2954fb36e35797656"
],
"version": "==0.14.7"
},
"attrs": {
"hashes": [
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.1.0"
"version": "==19.3.0"
},
"beaker": {
"hashes": [
"sha256:ad5d1c05027ee3be3a482ea39f8cb70339b41e5d6ace0cb861382754076d187e"
],
"version": "==1.11.0"
},
"beautifulsoup4": {
"hashes": [
"sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a",
"sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887",
"sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"
],
"index": "pypi",
"version": "==4.8.2"
},
"certifi": {
"hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
"version": "==2019.11.28"
},
"cffi": {
"hashes": [
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
],
"version": "==1.14.0"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"version": "==7.0"
"version": "==7.1.1"
},
"coloredlogs": {
"hashes": [
"sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a",
"sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505"
],
"index": "pypi",
"version": "==14.0"
},
"coverage": {
"hashes": [
"sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
"sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
"sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
"sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
"sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
"sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
"sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
"sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
"sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
"sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
"sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
"sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
"sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
"sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
"sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
"sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
"sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
"sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
"sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
"sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
"sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
"sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
"sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
"sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
"sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
"sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
"sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
"sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
"sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
"sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
"sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
],
"index": "pypi",
"version": "==5.0.3"
},
"cryptography": {
"hashes": [
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
],
"version": "==2.8"
},
"decorator": {
"hashes": [
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
],
"version": "==4.4.2"
},
"defusedxml": {
"hashes": [
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
],
"version": "==0.6.0"
},
"dnspython": {
"hashes": [
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
],
"version": "==1.16.0"
},
"ecdsa": {
"hashes": [
"sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061",
"sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277"
],
"version": "==0.15"
},
"eventlet": {
"hashes": [
"sha256:658b1cd80937adc1a4860de2841e0528f64e2ca672885c4e00fc0e2217bde6b1",
"sha256:6c9c625af48424c4680d89314dbe45a76cc990cf002489f9469ff214b044ffc1"
],
"index": "pypi",
"version": "==0.25.1"
},
"ffmpeg-python": {
"hashes": [
"sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127",
"sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"
],
"index": "pypi",
"version": "==0.2.0"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.0.2"
"version": "==1.1.1"
},
"flask-cors": {
"hashes": [
"sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
"sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
],
"index": "pypi",
"version": "==3.0.8"
},
"flask-httpauth": {
"hashes": [
"sha256:c08b69b302f1aa7ecd0db327809132ef6ca9486a36a9174776da146d1a4adc18",
"sha256:f71b7611f385fbdf350e8c430eed17b41c3b2200dc35eae19c1734264b68e31d"
"sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72",
"sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9"
],
"index": "pypi",
"version": "==3.2.4"
"version": "==3.3.0"
},
"flask-jwt-extended": {
"hashes": [
"sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd"
],
"index": "pypi",
"version": "==3.24.1"
},
"flask-login": {
"hashes": [
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
"sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
"sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
],
"index": "pypi",
"version": "==0.4.1"
"version": "==0.5.0"
},
"flask-marshmallow": {
"flask-migrate": {
"hashes": [
"sha256:75c9d80f22af982b1e8ccec109d3b75c14bb5570602ae3705a4ff775badd2816",
"sha256:db7aff4130eb99fd05ab78fd2e2c58843ba0f208899aeb1c14aff9cd98ae8c80"
],
"version": "==0.9.0"
},
"flask-restplus": {
"hashes": [
"sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62",
"sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69"
],
"version": "==0.12.1"
},
"flask-restplus-patched": {
"hashes": [
"sha256:36342775f9e0990dfc000dbe61133dfe56f9ef32c9b4c6293ba7f2c128d16efc"
"sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1",
"sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502"
],
"index": "pypi",
"version": "==0.1.10"
"version": "==2.5.2"
},
"flask-pyoidc": {
"hashes": [
"sha256:215f91ec5f08d7a40b5fc10f88bec06bf39840f9242bb18e0ead3a249c1329ee"
],
"index": "pypi",
"version": "==3.2.0"
},
"flask-restx": {
"hashes": [
"sha256:641759fe7cba1cb073d15b4258b1b15840af8cffe6edbd0da3e6b61eae0a67a6",
"sha256:fe0845112014201d618c1b0f3e41fa5f818a17c1d9e3fb6600d4e2c2bbc76a42"
],
"index": "pypi",
"version": "==0.1.1"
},
"flask-script": {
"hashes": [
"sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65"
],
"index": "pypi",
"version": "==2.0.6"
},
"flask-socketio": {
"hashes": [
"sha256:2172dff1e42415ba480cee02c30c2fc833671ff326f1598ee3d69aa02cf768ec",
"sha256:7ff5b2f5edde23e875a8b0abf868584e5706e11741557449bc5147df2cd78268"
],
"index": "pypi",
"version": "==4.2.1"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
"sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327",
"sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"
],
"index": "pypi",
"version": "==2.3.2"
"version": "==2.4.1"
},
"flask-testing": {
"hashes": [
"sha256:d849bf53eb1ceb09dff6611888588cb60f20238058fb1ebcd917d69febc373e6"
],
"index": "pypi",
"version": "==0.8.0"
},
"future": {
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"version": "==0.18.2"
},
"greenlet": {
"hashes": [
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
"sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e",
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
"sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122",
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656",
"sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"
],
"version": "==0.4.15"
},
"html5lib": {
"hashes": [
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
],
"index": "pypi",
"version": "==1.0.1"
},
"humanfriendly": {
"hashes": [
"sha256:25c2108a45cfd1e8fbe9cdb30b825d34ef5d5675c8e11e4775c9aedbfb0bdee2",
"sha256:3a831920e40e55ad49adb64c9179ed50c604cabca72cd300e7bd5b51310e4ebb"
],
"version": "==8.1"
},
"ics": {
"hashes": [
"sha256:bf5fbdef6e1e073afdadf1b996f0271186dd114a148e38e795919a1ae644d6ac"
],
"index": "pypi",
"version": "==0.7"
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
"version": "==2.9"
},
"importlib-metadata": {
"hashes": [
"sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
"sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
],
"markers": "python_version < '3.8'",
"version": "==1.5.0"
},
"importlib-resources": {
"hashes": [
"sha256:1dff36d42d94bd523eeb847c25c7dd327cb56686d74a26dfcc8d67c504922d59",
"sha256:7f0e1b2b5f3981e39c52da0f99b2955353c5a139c314994d1126c2551ace9bdf"
],
"version": "==1.3.1"
},
"itsdangerous": {
"hashes": [
@@ -105,17 +413,24 @@
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"
],
"version": "==2.10"
"version": "==2.11.1"
},
"jsonschema": {
"hashes": [
"sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d",
"sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"
"sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
"sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
],
"version": "==3.0.1"
"version": "==3.2.0"
},
"mako": {
"hashes": [
"sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d",
"sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"
],
"version": "==1.1.2"
},
"markupsafe": {
"hashes": [
@@ -123,13 +438,16 @@
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
@@ -146,24 +464,131 @@
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"marshmallow": {
"monotonic": {
"hashes": [
"sha256:6eeaf1301a5f5942bfe8ab2c2eaf03feb793072b56d5fae563638bddd7bb62e6",
"sha256:f72a206432a3369dd72824564d18d915761e07805c05f00d0dcc7885fac1e385"
"sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0",
"sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7"
],
"version": "==2.18.1"
"version": "==1.5"
},
"oic": {
"hashes": [
"sha256:091b20c0a4866e5afeef8fc21bfdffd65382763f09d782e14f8ce9081326e1ed",
"sha256:865da7ade1291c2f39dd196c34e5641a782b29871c3a48289e317d62fa49ef20"
],
"version": "==1.1.2"
},
"passlib": {
"hashes": [
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
"sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177",
"sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"
],
"index": "pypi",
"version": "==1.7.1"
"version": "==1.7.2"
},
"pbr": {
"hashes": [
"sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
"sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
],
"version": "==5.4.4"
},
"pillow": {
"hashes": [
"sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be",
"sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946",
"sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837",
"sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f",
"sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00",
"sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d",
"sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533",
"sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a",
"sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358",
"sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda",
"sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435",
"sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2",
"sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313",
"sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff",
"sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317",
"sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2",
"sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614",
"sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0",
"sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386",
"sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9",
"sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636",
"sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"
],
"index": "pypi",
"version": "==7.0.0"
},
"pyasn1": {
"hashes": [
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
],
"version": "==0.4.8"
},
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"version": "==2.20"
},
"pycryptodomex": {
"hashes": [
"sha256:1537d2d15b604b303aef56e7f440895a1c81adbee786b91f1f06eddc34da5314",
"sha256:1d20ab8369b7558168fc014a0745c678613f9f486dae468cca2d68145196b8a4",
"sha256:1ecc9db7409db67765eb008e558879d298406642d33ade43a6488224d23e8081",
"sha256:37033976f72af829fe15f7fe5fe1dbed308cc43a98d9dd9d2a0a76de8ca5ee78",
"sha256:3c3dd9d4c9c1e279d3945ae422895c901f98987333acc132dc094faf52afec35",
"sha256:3c9b3fba037ea52c626060c5a87ee6de7e86c99e8a7c6ee07302539985d2bd64",
"sha256:45ee555fc5e28c119a46d44ce373f5237e54a35c61b750fb3a94446b09855dbc",
"sha256:4c93038ac011b36512cb0bf2ee3e2aec774e8bc81021d015917c89fe02bb0ee5",
"sha256:50163324834edd0c9ce3e4512ded3e221c969086e10fdd5d3fdcaadac5e24a78",
"sha256:59b0ea9cda5490f924771456912a225d8d9e678891f9f986661af718534719b2",
"sha256:5cf306a17cccc327a33cdc3845629fa13f4573a4ec620ed607c79cf6785f2e27",
"sha256:5fff8da399af16a1855f58771223acbbdac720b9969cd03fc5013d2e9a7bd9a4",
"sha256:68650ce5b9f7152b8283302a4617269f821695a612692640dd247bd12ab21c0b",
"sha256:6b3a9a562688996f760b5077714c3ab8b62ca56061b6e9ab7906841e43e19f91",
"sha256:7e938ed51a59e29431ea86fab60423ada2757728db0f78952329fa02a789bd31",
"sha256:87aa70daad6f039e814790a06422a3189311198b674b62f13933a2bdcb6b1bcc",
"sha256:99be3a1df2b2b9f731ebe1c264a2c07c465e71cee68e35e1640b645b5213a755",
"sha256:a3f2908666e6f74b8c4893f86dd02e16170f50e4a78ae7f3468b6208d54bc205",
"sha256:ae3d44a639fd11dbdeca47e35e94febb1ee8bc15daf26673331add37146e0b85",
"sha256:afb4c2fa3c6f492fd9a8b38d76e13f32d429b8e5e1e00238309391b5591cde0d",
"sha256:b1515ce3a8a2c3fa537d137c5ca5f8b7a902044d04e07d7c3aa26c3e026120fb",
"sha256:bf391b377413a197000b43ef2b74359974d8927d329a897c9f5ba7b63dca7b9c",
"sha256:c436919117c23355740c669f89720673578b9aa4569bbfe105f6c10101fc1966",
"sha256:d2c3c280975638e2a2c2fd9cb36ab111980219757fa163a2755594b9448e4138",
"sha256:e585d530764c459cbd5d460aed0288807bb881f376ca9a20e653645217895961",
"sha256:e76e6638ead4a7d93262a24218f0ff3ff74de6b6c823b7e19dccb31b6a481978",
"sha256:ebfc2f885cafda076c31ae30fa0dd81e7e919ec34059a88d3018ed66e83fcce3",
"sha256:f5797a39933a3d41526da60856735e6684b2b71a8ca99d5f79555ca121be2f4b",
"sha256:f7e5fc5e124200b19a14be33fb0099e956e6ebb5e25d287b0829ef0a78ed76c7",
"sha256:fb350e31e55211fec8ddc89fc0256f3b9bc3b44b68a8bde1cf44b3b4e80c0e42"
],
"version": "==3.9.7"
},
"pydub": {
"hashes": [
"sha256:c362fa02da1eebd1d08bd47aa9b0102582dff7ca2269dbe9e043d228a0c1ea93",
"sha256:d29901a486fb421c5d7b0f3d5d3a60527179204d8ffb20e74e1ae81c17e81b46"
],
"index": "pypi",
"version": "==0.23.1"
},
"pyjwkest": {
"hashes": [
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
],
"version": "==1.4.2"
},
"pyjwt": {
"hashes": [
@@ -175,51 +600,246 @@
},
"pyrsistent": {
"hashes": [
"sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"
"sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280"
],
"version": "==0.14.11"
"version": "==0.15.7"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"version": "==2.8.1"
},
"python-editor": {
"hashes": [
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
],
"version": "==1.0.4"
},
"python-engineio": {
"hashes": [
"sha256:47ae4a9b3b4f2e8a68929f37a518338838e119f24c9a9121af92c49f8bea55c3",
"sha256:c3a3822deb51fdf9c7fe4d78abf807c73b83ea538036a50862d3024450746253"
],
"version": "==3.11.2"
},
"python-jose": {
"hashes": [
"sha256:1ac4caf4bfebd5a70cf5bd82702ed850db69b0b6e1d0ae7368e5f99ac01c9571",
"sha256:8484b7fdb6962e9d242cce7680469ecf92bda95d10bbcbbeb560cacdff3abfce"
],
"index": "pypi",
"version": "==3.1.0"
},
"python-socketio": {
"extras": [
"client"
],
"hashes": [
"sha256:48cba5b827ac665dbf923a4f5ec590812aed5299a831fc43576a9af346272534",
"sha256:af6c23c35497960f82106e36688123ecb52ad5a77d0ca27954ff3811c4d9d562"
],
"index": "pypi",
"version": "==4.4.0"
},
"pythonping": {
"hashes": [
"sha256:05269d459d2290ff57665aa3b3c9ed1b64bb96106d22712e0054b52d51c6bb13"
],
"index": "pypi",
"version": "==1.0.8"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2018.9"
"version": "==2019.3"
},
"relativetimebuilder": {
"requests": {
"hashes": [
"sha256:5cc415b539d18a20e09a600cf7ba7199eda7b365d13aaaf9ffbbaa26cfb8062a",
"sha256:8b11e6fa6d6d4a09c61cfa9dadae4ea640bf10818e0991874d33452c0aeff2d7"
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"version": "==0.2.0"
"version": "==2.23.0"
},
"rsa": {
"hashes": [
"sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66",
"sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487"
],
"version": "==4.0"
},
"scapy": {
"hashes": [
"sha256:e2f8d11f6a941c14a789ae8b236b27bd634681f1b29b5e893861e284d234f6b0"
],
"index": "pypi",
"version": "==2.4.3"
},
"simpleaudio": {
"hashes": [
"sha256:05b63da515f5fc7c6f40e4d9673d22239c5e03e2bda200fc09fd21c185d73713",
"sha256:67348e3d3ccbae73bd126beed7f1e242976889620dbc6974c36800cd286430fc",
"sha256:691c88649243544db717e7edf6a9831df112104e1aefb5f6038a5d071e8cf41d",
"sha256:86f1b0985629852afe67259ac6c24905ca731cb202a6e96b818865c56ced0c27",
"sha256:f1a4fe3358429b2ea3181fd782e4c4fff5c123ca86ec7fc29e01ee9acd8a227a",
"sha256:f346a4eac9cdbb1b3f3d0995095b7e86c12219964c022f4d920c22f6ca05fb4c",
"sha256:f68820297ad51577e3a77369e7e9b23989d30d5ae923bf34c92cf983c04ade04"
],
"index": "pypi",
"version": "==1.0.4"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.12.0"
"version": "==1.14.0"
},
"socketio-client": {
"hashes": [
"sha256:64cd84fba79cf97f28c11e64d1fc42a2221f2d7a4fada05ab381e2d73d74d2c1"
],
"index": "pypi",
"version": "==0.7.2"
},
"soupsieve": {
"hashes": [
"sha256:e914534802d7ffd233242b785229d5ba0766a7f487385e3f714446a07bf540ae",
"sha256:fcd71e08c0aee99aca1b73f45478549ee7e7fc006d51b37bec9e9def7dc22b69"
],
"version": "==2.0"
},
"sqlalchemy": {
"hashes": [
"sha256:11ead7047ff3f394ed0d4b62aded6c5d970a9b718e1dc6add9f5e79442cc5b14"
"sha256:c4cca4aed606297afbe90d4306b49ad3a4cd36feb3f87e4bfd655c57fd9ef445"
],
"version": "==1.3.0"
"index": "pypi",
"version": "==1.3.15"
},
"webargs": {
"sqlalchemy-migrate": {
"hashes": [
"sha256:10438164b41b81abe45b299eb182580f7bc6bcdbc864b0cbd62845bb6bab424d",
"sha256:3bed01136ea4a7d1468a54f6c3925d133872a83a2144e83a94f484731576bc58",
"sha256:494044344b5673e3624621d0e9d14d5dc01dd05c0b5b8952febc80a4f80181f6"
"sha256:0bc02e292a040ade5e35a01d3ea744119e1309cdddb704fdb99bac13236614f8",
"sha256:e5d2348db19a5062132d93e3b4d9e7644af552fffbec4c78cc5358f848d2f6c1"
],
"version": "==5.1.2"
"index": "pypi",
"version": "==0.13.0"
},
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
],
"version": "==0.3.1"
},
"ssh2-python": {
"hashes": [
"sha256:054f2cab611ddca34095eb78d1ae1f6e29521678c82f0de17f696072a0b924fe",
"sha256:097d74684172f163d5025aef316987c1c7acf852d9fb75e4735720c8690adf87",
"sha256:0f4555b79e19ca6fdd84a74e85152d375a921d167ed9dc680d05f1f9f5dfc463",
"sha256:0fb3d9eca454b2874748c69e7e836ea5c6544f1b7acd87d8a876f99f618c806c",
"sha256:180c2728796f777a5856b23e5e1656a15a3e8602b4270348f892599ae5426f45",
"sha256:49aaa9d48cf52798d89f466a5e774e9dab8b76dc380b90645fd8d5c6622df177",
"sha256:4b5ba1390ff5e8faa6b2409df750e4650de3934c3b76ad707d7b54b477600226",
"sha256:57a4452dfc5c7f414de14e32f29e21f680fb8d7bdf94306e5f6bc8c2a235fa7a",
"sha256:584f753f0401217a55e70d2ae97a7e5220f4d37993a1bba275c4c5e110d87193",
"sha256:692cf27ddc5583da9b07b0f0348f0577579f8d798a0b8e4bf495b8d7f19fc854",
"sha256:6dc75bacc37de63aa9c86f9982642982c3a8103297fbbb73543a76e3db9abc6a",
"sha256:7a029275e62a1e31333ee7f2f151f661dedfac0345f1108e1a8d0b62e08ddf9b",
"sha256:7a3d761c669392e778c24a0eabcf6f1ba48ad68bb3b82a2d98235f56500bc8b0",
"sha256:8632684aae2f523e603381cf51a1da134fd5937cf0ae9650b1b9e7b4f328c133",
"sha256:8acab66dd73554d2c68abaccf96cdddfc10449ad6c7c273ac4b7a91779d414b2",
"sha256:9410b63c2361b2b65a55b5e8dc8187270de0f81e11864a427ad52b82b926bcc9",
"sha256:abe29ad01ed2ac4465e61c8de42b6850fca9c87d8792686a2de7c7d06c6eb72d",
"sha256:de151500a2a027a29bca3d7196c76008b41bb97db29378f29c4651ba1bcb00e8",
"sha256:debc56711643b82ab60a9c8b8cbf2978b65c0293aa7e53557a3c0d1fc57799cf",
"sha256:f508144d684b3c3c0f264ce67058f05ae3729ae77cc3a8f2051b442e01594849"
],
"index": "pypi",
"version": "==0.18.0.post1"
},
"style": {
"hashes": [
"sha256:6485a4bcb84629341a5fd1587fe3ac4887daa4741f0c8a1d01b9c3c8a263afe7",
"sha256:8eb365fc15039b19b728bd4e6e85fb7daf24e7aeeec6a15a666f97484c564005"
],
"version": "==1.1.0"
},
"tatsu": {
"hashes": [
"sha256:80713413473a009f2081148d0f494884cabaf9d6866b71f2a68a92b6442f343d",
"sha256:c9211eeee9a2d4c90f69879ec0b518b1aa0d9450249cb0dd181f5f5b18be0a92"
],
"version": "==4.4.0"
},
"tempita": {
"hashes": [
"sha256:cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c"
],
"version": "==0.5.2"
},
"typing-extensions": {
"hashes": [
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
],
"version": "==3.7.4.1"
},
"tzlocal": {
"hashes": [
"sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048",
"sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"
],
"version": "==2.0.0"
},
"update": {
"hashes": [
"sha256:a25522b4bf60e3e3c1a3ff3ca3a4f5a328ac4b8ff400fdc9614483147e313323"
],
"index": "pypi",
"version": "==0.0.1"
},
"urllib3": {
"hashes": [
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
"version": "==1.25.8"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
"websocket-client": {
"hashes": [
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
],
"index": "pypi",
"version": "==0.57.0"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
"sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2",
"sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"
],
"version": "==0.14.1"
"version": "==0.16.1"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"markers": "python_version < '3.8'",
"version": "==3.1.0"
}
},
"develop": {}

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
Backend code for LRC
requirements:
- python > 3.6
- libasound2-dev

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
"""
Backend base module
"""
from flask import Flask
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
from flask_sqlalchemy import SQLAlchemy
jwt_auth = HTTPTokenAuth()
basic_auth = HTTPBasicAuth()
multi_auth = MultiAuth(basic_auth, jwt_auth)
from .serve_frontend import fe_bp
from .api import api_bp
app = Flask(__name__)
app.register_blueprint(api_bp)
app.register_blueprint(fe_bp)
db = SQLAlchemy(app)

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2019. Tobias Kurze
from backend import app
def main():
app.run(debug=True)
if __name__ == '__main__':
main()

View File

@@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
from flask import Blueprint
from flask_restplus import Api
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')
from .example_api import *

182
backend/__init__.py Normal file
View File

@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
"""
Backend base module
"""
import logging
from io import StringIO
from logging.config import dictConfig
from logging.handlers import MemoryHandler
from typing import Union
import coloredlogs as coloredlogs
import jwt
import requests
from flask import Flask, jsonify, abort
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
from backend.config import Config
__author__ = "Tobias Kurze"
__copyright__ = "Copyright 2019, Tobias Kurze, KIT"
__credits__ = ["Tobias Kurze"]
__license__ = ""
__version__ = "0.9.0"
__maintainer__ = "Tobias Kurze"
__email__ = "it@t-kurze.de"
# __status__ = "Production"
__status__ = "Development"
from .tools.send_mail import get_smtp_default_handler
dictConfig({
'version': 1,
'formatters': {
'default': {
'format': '[%(asctime)s] {%(threadName)s} %(levelname)s in %(module)s, line %(lineno)d: %(message)s',
}
},
'handlers': {
'wsgi': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default'
},
'root_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': Config.ROOT_LOG_FILE,
'when': 'd',
'interval': 1,
'backupCount': 3,
'formatter': 'default',
},
'file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': Config.LOG_FILE,
'when': 'd',
'interval': 1,
'backupCount': 5,
'formatter': 'default',
},
'errors_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': Config.ERROR_LOG_FILE,
'when': 'd',
'interval': 1,
'backupCount': 5,
'level': 'ERROR',
'formatter': 'default',
},
},
'loggers': {
'lrc': {
'level': Config.LOG_LEVEL,
'handlers': ['wsgi', 'file', 'errors_file']
}
},
'root': {
'level': 'ERROR',
'handlers': ['root_file', 'errors_file']
}
})
main_logger = logging.getLogger("lrc")
# following might be dangerous, as buffer might be filled without a mechanism to empty it
smtp_error_handler = get_smtp_default_handler(subject="Warnings, errors and worse...!")
mem_handler = MemoryHandler(capacity=10, flushLevel=logging.ERROR, target=smtp_error_handler)
mem_handler.setLevel(logging.WARNING)
# error_log_stream = StringIO()
# error_log_stream_handler = logging.StreamHandler(stream=error_log_stream)
# error_log_stream_handler.setLevel(logging.ERROR)
# main_logger.addHandler(error_log_stream_handler)
coloredlogs.install(level='DEBUG', logger=main_logger)
class LrcException(Exception):
def __init__(self, message_or_exception: Union[str, Exception], html_code: int = None):
if isinstance(message_or_exception, str):
super().__init__(message_or_exception)
self.type = None
else:
super().__init__(str(message_or_exception))
self.type = type(message_or_exception)
self.html_code = html_code
def __repr__(self):
if self.type is None:
msg = "LRC Exception: \"{}\"".format(', '.join(super().args))
else:
msg = "LRC Exception: (original Exception: {}) \"{}\"".format(self.type, ', '.join(super().args))
if self.html_code is not None:
msg += " (HTML Code: {})".format(self.html_code)
return msg
def __str__(self):
return self.__repr__()
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)
# this is another library.... this is (probaby ->verify) used to check JWTs provided by external sources (KIT, etc.)
jwt_auth = HTTPTokenAuth('Bearer')
@jwt_extended.invalid_token_loader
def unauthorized_jwt(token):
main_logger.info("Unauthorized access; invalid token provided: {}".format(token))
abort(401)
@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 jwt_extended by duck typing error handlers
jwt_extended._set_error_handler_callbacks(api_v1)

68
backend/__main__.py Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2019. Tobias Kurze
import logging
import ssl
import sys
from jinja2.exceptions import TemplateNotFound
from backend import app, db
from backend.cron import get_default_scheduler, add_default_jobs
from backend.models import *
from backend.models import room_model, recorder_model, RecorderCommand, Recorder
from backend.recorder_adapters import get_defined_recorder_adapters
from backend.tools.model_updater import update_recorder_models_database, create_default_recorders
from backend.websocket.base import WebSocketBase
def main():
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
#db.drop_all()
#db.create_all()
#Recorder()
room_model.pre_fill_table()
update_recorder_models_database(drop=False)
create_default_recorders()
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)
print("Starting Scheduler")
scheduler = get_default_scheduler()
add_default_jobs(scheduler)
scheduler.start()
wsb = WebSocketBase()
print("running websocket...(replaces normal app.run()")
wsb.start_websocket(debug=True)
# print("running web app...")
#app.run(debug=True, host="0.0.0.0", threaded=True)
wsb.send_test_msg()
while True:
user_in = input("Type >exit< to quit.")
if user_in == "exit" or user_in == ">exit<":
break
scheduler.shutdown()
sys.exit(0)
if __name__ == '__main__':
main()

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

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, abort
from flask_restx 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_permissions = Namespace('permissions', description="Permissions 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_state = Namespace('state', description="Object state 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_permissions)
api_v1.add_namespace(api_room)
api_v1.add_namespace(api_recorder)
api_v1.add_namespace(api_state)
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 .permission_api import *
from .group_api import *
from .room_api import *
from .recorder_api import *
from .state_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_restx 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,55 @@
# 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_restx import fields, Resource
from backend import db
from backend.api import api_control, get_jwt_identity, Recorder, RecorderCommand, pprint
from backend.recorder_adapters.helpers import execute_recorder_command
control_command_response_model = api_control.model('Control Command Response', {
'time': fields.DateTime(required=False, description='Creation date of the recorder'),
'ok': fields.Boolean(required=True, description='Field indicating whether command execution was successful.'),
'output': fields.String(required=False, description='Command output in case of success'),
'error': fields.String(required=False, description='Error description in case of a problem.'),
})
@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={}, 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()}
#TODO: right check! (acl, etc.)
args = self.control_command_parser.parse_args()
recorder = Recorder.get_by_identifier(args.get('recorder_id'))
if recorder is None:
api_control.abort(404, "Recorder not found!")
command = RecorderCommand.get_by_identifier(args.get('command_id'))
if command is None:
api_control.abort(404, "Command not found!")
success, output = execute_recorder_command(recorder, command, args.get('parameters', None))
if success:
return {'time': datetime.utcnow(), 'output': output, 'ok': success}
return {'time': datetime.utcnow(), 'error': output, 'ok': success}

View File

@@ -1,9 +1,13 @@
import datetime
import ipaddress
import json
import logging
from random import *
from flask import jsonify, Blueprint
from flask_restplus import Resource, reqparse
from flask import jsonify, Blueprint, request
from flask_restx import Resource, reqparse
from backend import basic_auth, multi_auth
from backend import basic_auth, multi_auth, db, jwt_auth
from backend.api import api_v1, api_bp
@@ -18,6 +22,18 @@ def random_number():
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.
@@ -59,7 +75,7 @@ class SensorData_Handler(Resource):
print("values...")
print(args['values'])
values = json.loads(args['values'])
wasss_app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
rough_geo_location = None
try:

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

@@ -0,0 +1,91 @@
# 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_restx 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.'),
'permissions': fields.List(fields.Nested(api_group.model('group_permissions',
{'id': fields.Integer,
'name': fields.String})),
required=False, description='Group permissions.'),
})
@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

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

@@ -0,0 +1,107 @@
from flask_restx import fields
from backend.api import api_user, api_recorder, api_v1, api_state
generic_id_parser = api_v1.parser()
generic_id_parser.add_argument('id', type=str, required=True, store_missing=False)
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'),
'last_seen': fields.DateTime(required=False, description='Last time user logged in'),
'last_time_modified': fields.DateTime(required=False, description='Last time user was modified'),
'role': fields.String(required=False, description='Role a user might have (in addition to group memberships)'),
'effective_permissions': fields.List(
fields.String(required=True), required=False, description="List of permissions (groups + (optional) role)."
),
'groups': fields.List(
fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})),
required=False, description='Group memberships.'),
'favorite_recorders': fields.List(
fields.Nested(api_user.model('favorite_recorder',
{'id': fields.Integer(), 'name': fields.String(), 'offline': fields.Boolean(),
'created_at': fields.DateTime(), 'last_time_modified': fields.DateTime()})),
required=False, description='Favorite recorders.'),
})
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'),
'model_name': fields.String(min_length=3, required=True,
description='The recorder\'s model name (might slightly '
'differ from actual name of the model)'),
'serial_number': fields.String(required=False, description='The recorder\'s serial number'),
'firmware_version': fields.String(required=False, description='The recorder\'s firmware'),
'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.'),
'additional_camera_connected': fields.Boolean(required=False,
description='Indicates whether an additional camera is connected'),
'ip': fields.String(required=False, description='The recorder\'s IP address'),
'mac': 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")
})
state_model = api_state.model('Recorder', {
})

View File

@@ -0,0 +1,91 @@
# 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_restx import fields, Resource
from backend import db
from backend.api import api_permissions
from backend.models.user_model import Permission
permission_model = api_permissions.model('Permission', {
'id': fields.String(required=False, description='The permission\'s identifier'),
'name': fields.String(required=True, description='The permission\'s name'),
'description': fields.String(required=False, description='The permission\'s description'),
'groups': fields.List(fields.Nested(api_permissions.model('group_member',
{'id': fields.Integer(),
'name': fields.String(),
'description': fields.String()})),
required=False, description='Groups having the permission.'),
'access_control_entry': fields.Nested(api_permissions.model('group_member',
{'id': fields.Integer(),
'name': fields.String(),
'url': fields.String()}),
required=False, description="Access Control Entry"),
})
@api_permissions.route('/<int:id>')
@api_permissions.response(404, 'permission not found')
@api_permissions.param('id', 'The permission identifier')
class PermissionResource(Resource):
@jwt_required
@api_permissions.doc('get_permission')
@api_permissions.marshal_with(permission_model)
def get(self, id):
"""Fetch a user given its identifier"""
permission = Permission.get_by_id(id)
if permission is not None:
return permission
api_permissions.abort(404)
@jwt_required
@api_permissions.doc('delete_permission')
@api_permissions.response(204, 'permission deleted')
def delete(self, id):
"""Delete a permission given its identifier"""
permission = Permission.get_by_id(id)
if permission is not None:
permission.delete()
return '', 204
api_permissions.abort(404)
@jwt_required
@api_permissions.doc('update_permission')
@api_permissions.expect(permission_model)
@api_permissions.marshal_with(permission_model)
def put(self, id):
"""Update a task given its identifier"""
permission = Permission.get_by_id(id)
if permission is not None:
permission.name = api_permissions["name"]
db.session.commit()
return permission
api_permissions.abort(404)
@api_permissions.route('')
class PermissionList(Resource):
@jwt_required
@api_permissions.doc('permissions')
@api_permissions.marshal_list_with(permission_model)
def get(self):
"""
List all permissions
:return: permissions
"""
return Permission.get_all()
@jwt_required
@api_permissions.doc('create_permission')
@api_permissions.expect(permission_model)
@api_permissions.marshal_with(permission_model, code=201)
def post(self):
permission = Permission(**api_permissions.payload)
db.session.add(permission)
db.session.commit()
return permission

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

@@ -0,0 +1,205 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to recorders through the API.
"""
from datetime import datetime
from pprint import pprint
from flask_jwt_extended import jwt_required
from flask_restx import fields, Resource, inputs
from backend import db, app
from backend.api import api_recorder
from backend.api.models import recorder_model, recorder_model_model, recorder_command_model
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
from backend.models.room_model import Room
import backend.recorder_adapters as r_a
# ==
@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_restx 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

67
backend/api/state_api.py Normal file
View File

@@ -0,0 +1,67 @@
# Copyright (c) 2019. Tobias Kurze
"""
This module provides functions related to object states through the API.
"""
from datetime import datetime
from pprint import pprint
from flask_jwt_extended import jwt_required
from flask_restx import fields, Resource, inputs
from backend import db, app
from backend.api import api_state
from backend.api.models import recorder_model, recorder_model_model, recorder_command_model, state_model
from backend.cron import async_cron_recorder_checker
from backend.cron.cron_state_checker import StateChecker
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
from backend.models.room_model import Room
import backend.recorder_adapters as r_a
# ==
from backend.tools.recorder_state_checker import get_recorder_adapter, check_capture_agent_state, check_stream_sanity
@api_state.route('/recorder/<int:id>')
@api_state.response(404, 'Recorder not found')
@api_state.param('id', 'The recorder identifier')
class RecorderStateResource(Resource):
@jwt_required
@api_state.doc('get_recorder_state')
@api_state.marshal_with(state_model, skip_none=False)
def get(self, id):
"""Fetch a recorder given its identifier"""
recorder: Recorder = Recorder.query.get(id)
if recorder is None:
api_state.abort(404)
current_states_by_checker = async_cron_recorder_checker.get_current_state()
state = async_cron_recorder_checker.get_current_state_for_recorder_id(recorder.id)
if state is None:
state_checker = StateChecker([check_capture_agent_state], Recorder)
state_checker.add_object_to_state_check(recorder)
state_checker.check_object_state()
state = state_checker.get_current_state_for_recorder_id(recorder.id)
if not state.get('state_ok', False): # if state is not OK, return state -> no more checks!
return state
# do additional checks, such as: check for single color, sound check, etc.
stream_state = check_stream_sanity(recorder)
print(stream_state)
return stream_state
@api_state.route('/recorder')
class RecorderStateList(Resource):
@jwt_required
@api_state.doc('get_recorders_states')
@api_state.marshal_list_with(state_model, skip_none=False)
def get(self):
"""
Get state of all recorders
:return: state
"""
return Recorder.get_all()

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

@@ -0,0 +1,129 @@
# 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_restx import Resource, fields, inputs, abort
from backend import db, app, jwt_auth
from backend.api import api_user
from backend.api.models import user_model, recorder_model, generic_id_parser
from backend.models import Recorder
from backend.models.user_model import User, Group
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('/profile/favorite_recorders')
class UserFavoriteRecorders(Resource):
@jwt_required
@api_user.marshal_list_with(recorder_model)
def get(self):
try:
current_user_id = get_jwt_identity()
return User.get_by_identifier(current_user_id).favorite_recorders
except AttributeError:
abort(404, "User not found!")
@jwt_required
@api_user.expect(generic_id_parser)
@api_user.marshal_list_with(recorder_model)
def put(self):
try:
args = generic_id_parser.parse_args()
current_user_id = get_jwt_identity()
user = User.get_by_identifier(current_user_id)
print(args)
recorder = Recorder.get_by_identifier(args["id"])
print(recorder)
if recorder is None:
abort(404, "(Specified [id: {}]) recorder not found!".format(args["id"]))
user.favorite_recorders.append(recorder)
db.session.commit()
return user.favorite_recorders
except AttributeError:
abort(404, "User not found!")
@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_restx 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"

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

@@ -0,0 +1,80 @@
# Copyright (c) 2019. Tobias Kurze
"""
OIDC login auth module
"""
from datetime import datetime
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")
user.last_seen = datetime.utcnow()
# TODO: update user!
db.session.commit()
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

BIN
backend/config.py Normal file

Binary file not shown.

98
backend/cron/__init__.py Normal file
View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
import logging
import random
import signal
import sys
import time
from logging.handlers import TimedRotatingFileHandler
from pprint import pprint
from backend import app, main_logger
from apscheduler.schedulers.background import BackgroundScheduler
from backend.cron.cron_state_checker import async_cron_recorder_checker
from backend.websocket.handlers import send_state_update_to_recorders
cron_log_handler = TimedRotatingFileHandler(app.config.get('CRON_LOG_FILE'), interval=1, when='d', backupCount=3)
cron_log_handler.setFormatter(logging.Formatter('[%(asctime)s] - %(funcName)20s() %(message)s'))
cron_logger = logging.getLogger("lrc.cron")
cron_logger.addHandler(cron_log_handler)
logging.getLogger("apscheduler.scheduler").addHandler(cron_log_handler)
logging.getLogger("apscheduler.executors.default").addHandler(cron_log_handler)
scheduler = None
def get_default_scheduler():
cron_logger.debug("creating scheduler!")
global scheduler
scheduler = BackgroundScheduler(timezone='utc')
scheduler.daemonic = False
return scheduler
def add_default_jobs(sched=None, testing=False):
global scheduler
if sched is None:
sched = scheduler
if testing:
check_recorder_state_job = sched.add_job(async_cron_recorder_checker.check_object_state, 'interval', seconds=40,
id="check_recorder_state_job")
else:
check_recorder_state_job = sched.add_job(async_cron_recorder_checker.check_object_state, 'interval', minutes=2,
id="check_recorder_state_job")
"""
Job regularly sending the state to "frontend recorders" through websocket
"""
send_update_state_to_recorder_job = sched.add_job(
lambda: send_state_update_to_recorders(async_cron_recorder_checker.get_current_state()), 'interval', minutes=1,
id="send_update_state_to_recorder_job")
return [check_recorder_state_job, send_update_state_to_recorder_job]
def signal_handler(sig, frame):
print('You pressed Ctrl+C -> shutting down scheduler!')
if scheduler is not None:
scheduler.shutdown()
sys.exit(0)
if __name__ == '__main__':
# check_for_ingestibles_and_ingest()
# remove_obsolete_media_files_objects()
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(
logging.Formatter('[%(asctime)s] {%(threadName)s} %(levelname)s in %(module)s, line %(lineno)d: %(message)s'))
cron_logger.addHandler(stream_handler)
cron_logger.setLevel(logging.INFO)
signal.signal(signal.SIGINT, signal_handler)
get_default_scheduler()
add_default_jobs(testing=True)
cron_logger.info("Starting internal scheduler")
scheduler.start()
c = 0
while c < 10:
sleep_time = random.randint(10, 20)
cron_logger.info("Sleeping for {}s".format(sleep_time))
time.sleep(sleep_time)
recorder_id = random.randint(0, 15)
cron_logger.info("Using recorder id {}".format(recorder_id))
async_cron_recorder_checker.add_object_to_state_check(recorder_id)
async_cron_recorder_checker.add_object_to_state_check(recorder_id + 1)
pprint(async_cron_recorder_checker.get_current_state())
while True:
user_in = input("Type >exit< to quit.")
if user_in == "exit" or user_in == ">exit<":
break
scheduler.shutdown()

View File

@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
import copy
import datetime
import logging
from multiprocessing.context import TimeoutError
from multiprocessing.pool import ThreadPool
from pprint import pprint
from threading import Lock
from typing import Union, Callable, TypeVar, Generic, Set, List
from backend.models import Recorder
from backend.tools.recorder_state_checker import check_capture_agent_state, ping_capture_agent
logger = logging.getLogger("lrc.cron.recorder_state")
recorder_jobs_lock = Lock()
recorder_jobs = set()
NUM_THREADS = 8
T = TypeVar('T')
class StateChecker(Generic[T]):
"""
This class is designed generically to regularly check the state of objects with given function(s).
The determined state is stored "locally" in the state checker object and NOT reflected back to the checked objects!
It can be retrieved by calling get_current_state.
"""
def __init__(self, state_checker_func: Union[Callable, List[Callable]], type_to_check: T, type_name=None,
threads=NUM_THREADS):
self.num_threads = threads
self.lock = Lock()
self.jobs: Set[T] = set()
self.checker_func = state_checker_func
self.checker_type = type_to_check
self.update_state_lock = Lock()
self.state_results = {}
self.type_name = type_name if type_name is not None else self.checker_type.__name__
def add_object_to_state_check(self, object_to_check: Union[int, T]):
if isinstance(object_to_check, int):
if not hasattr(self.checker_type, 'get_by_identifier'):
logger.error(
'Can\'t add object to state check, as >get_by_identifier< not defined on checker_type ({})!'.format(
str(self.checker_type)))
return
object_to_check = self.checker_type.get_by_identifier(object_to_check)
if object_to_check is None:
logger.warning(
"Could not add object ({}) to state check, as specified >id ({})< could not be found / object is None".format(
self.type_name, object_to_check))
return
with self.lock:
if hasattr(object_to_check, 'name'):
name = object_to_check.name
else:
name = str(object_to_check)
if any([j.id == object_to_check.id for j in self.jobs]):
logger.info(
"Not adding {} ({}) ({}) to state check (already in job list)".format(object_to_check.id, name,
self.type_name))
else:
logger.debug("Adding {} to object ({}) to state check".format(self.type_name, name))
self.jobs.add(object_to_check)
def remove_recorder_from_state_check(self, object_to_check: Union[int, T]):
if isinstance(object_to_check, int):
object_to_check = self.checker_type.get_by_identifier(object_to_check)
if object_to_check is None:
logger.warning(
"Could not remove object ({}) from state check, as specified id could not be found / object is None".format(
self.type_name))
return
self.lock.acquire()
if hasattr(object_to_check, 'name'):
name = object_to_check.name
else:
name = str(object_to_check)
logger.debug("Removing {} from object ({}) to state check".format(self.type_name, name))
self.jobs.remove(object_to_check)
self.lock.release()
def execute_checker_func(self, func, jobs: List[T], object_states: dict) -> dict:
with ThreadPool(self.num_threads) as pool:
results = [pool.apply_async(func, (job,)) for job in jobs]
try:
state_results = [res.get(timeout=12) for res in results]
for r in state_results:
if r[0]: # ok :)
if object_states[r[2]].get('msg', "") == "unknown state!":
del object_states[r[2]]['msg']
ok = True
else:
ok = object_states[r[2]].get('state_ok', False),
object_states[r[2]] = {
'id': object_states[r[2]].get('id', None),
'msg': ", ".join([s for s in [object_states[r[2]].get('msg', None), r[1]] if s]),
'state_ok': ok}
else:
object_states[r[2]] = {'id': object_states[r[2]].get('id', None),
'msg': r[1],
'state_ok': False}
except TimeoutError as e:
logger.error("Timeout while performing state check func! {}".format(e))
return object_states
def check_object_state(self) -> dict:
logger.info("checking object ({}) state...".format(self.type_name))
self.lock.acquire()
jobs = list(self.jobs)
self.lock.release()
if len(jobs) <= 0:
logger.info("No objects ({}) to check... returning".format(self.type_name))
return {}
logger.info("checking state of {} recorders".format(len(jobs)))
object_states = {j.name: {'id': j.id, 'state_ok': False, 'msg': 'unknown state!'} for j in jobs}
if isinstance(self.checker_func, list):
for c_f in self.checker_func:
self.execute_checker_func(c_f, jobs, object_states)
else:
self.execute_checker_func(self.checker_func, jobs, object_states)
self.update_state_dict(object_states)
return object_states
def update_state_dict(self, object_states: dict):
self.update_state_lock.acquire()
for o_s in object_states.keys():
if o_s in self.state_results:
# update existing state
self.state_results[o_s] = {**object_states[o_s],
'time_stamp': datetime.datetime.now(datetime.timezone.utc).strftime(
"%d.%m.%Y - %H:%M:%S %Z"),
'previous': {'state_ok': self.state_results[o_s]['state_ok'],
'msg': self.state_results[o_s].get('msg', None),
'time_stamp': self.state_results[o_s].get('time_stamp', None)}}
else:
self.state_results[o_s] = object_states[o_s]
self.update_state_lock.release()
def get_current_state(self):
with self.update_state_lock:
return copy.deepcopy(self.state_results)
def get_current_state_for_recorder_name(self, recorder_name: str):
return self.get_current_state().get(recorder_name, None)
def get_current_state_for_recorder_id(self, recorder_id: int):
states = self.get_current_state()
for key in states:
state = states[key]
if state.get('id', None) == recorder_id: # found!
return state
return None
async_cron_recorder_checker = StateChecker([check_capture_agent_state, ping_capture_agent], Recorder)

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,10 @@
"""
Import all models...
"""
from backend.models.access_control_model import *
from backend.models.example_model import *
from backend.models.post_model import *
from backend.models.recorder_model import *
from backend.models.room_model import *
from backend.models.user_model import *
from backend.models.virtual_command_model import *

View File

@@ -0,0 +1,73 @@
# -*- 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 AccessControlEntry(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)
url = db.Column(db.Unicode(2047), unique=False, nullable=True, default="")
required_permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))
required_permission = db.relationship('Permission', back_populates='access_control_entry')
__table_args__ = (
CheckConstraint('length(name) > 2',
name='name_min_length'),
)
def __init__(self, **kwargs):
super(AccessControlEntry, self).__init__(**kwargs)
@staticmethod
def get_by_name(name):
"""
Find ace by name
:param name:
:return:
"""
return AccessControlEntry.query.filter(AccessControlEntry.name == name).first()
@staticmethod
def get_all():
"""
Return all ace
:return:
"""
return AccessControlEntry.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():
a_es = {"url": "", }
access_entries = [AccessControlEntry(name=room['name'], number=room['room_number'],
building_name=room['building_name'], building_number=room['building_number']) for room in
a_es]
try:
db.session.bulk_save_objects(access_entries)
db.session.commit()
except IntegrityError as e:
db.session.rollback()

Binary file not shown.

View File

@@ -0,0 +1,11 @@
class ModelBase:
def get(self, attribute_name, default_value=None):
if hasattr(self, attribute_name):
return getattr(self, attribute_name)
elif False: # a check for properties?
pass
elif default_value is not None:
return default_value
else:
raise KeyError("{} not found".format(attribute_name))

View File

@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
"""
Models for lecture recorder
"""
import importlib
import ipaddress
import json
import pkgutil
import os
import re
from typing import Union
from sqlalchemy import MetaData, ForeignKeyConstraint
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import validates
from backend import db, app, login_manager, LrcException
from sqlalchemy import or_
from datetime import datetime, timedelta
from backend.models.model_base import ModelBase
from backend.models.virtual_command_model import virtual_command_recorder_command_table, virtual_command_recorder_table
metadata = MetaData()
# This is the association table for the many-to-many relationship between
# recorders and permissions.
recorder_permission_table = db.Table('recorder_permission',
db.Column('recorder_id', db.Integer,
db.ForeignKey('recorder.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),
extend_existing=True)
# This is the association table for the many-to-many relationship between
# recorders and recorderCommands and permissions.
recorder_rec_commands_permission_table = db.Table('recorder_rec_command_permission',
db.Column('recorder_id', db.Integer,
db.ForeignKey('recorder.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),
db.Column('permission_id', db.Integer,
db.ForeignKey('permission.id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
extend_existing=True)
class RecorderModel(db.Model, ModelBase):
__table_args__ = {'extend_existing': True}
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) # checksum of the recorder commands! (see: model_updater.py)
last_checksum_change = db.Column(db.DateTime, nullable=True, default=None)
_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_id(id):
return RecorderModel.query.filter(RecorderModel.id == id).first()
@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_where_adapter_id_contains(adapter_id):
return RecorderModel.query.filter(RecorderModel.record_adapter_id.contains(adapter_id)).first()
@staticmethod
def get_by_checksum(md5_sum):
return RecorderModel.query.filter(RecorderModel.checksum == md5_sum).first()
@hybrid_property
def requires_username(self):
return self._requires_user > 0
@requires_username.setter
def requires_username(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
def __str__(self):
return self.model_name + " (record adapter: {})".format(self.record_adapter_id)
class Recorder(db.Model, ModelBase):
__table_args__ = {'extend_existing': True}
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)
model_name = db.Column(db.Unicode(63), unique=False, nullable=False)
serial_number = db.Column(db.Unicode(63), unique=True, nullable=True)
locked = db.Column(db.Boolean, default=False) # no modifications allowed
lock_message = db.Column(db.String, nullable=True, default=None)
offline = db.Column(db.Boolean, default=False) # maintenance, etc.
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
_mac = db.Column(db.String(17), unique=True, nullable=True)
_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)
_configured_options_json_string = db.Column(db.UnicodeText, default='')
_additional_notes_json_string = db.Column(db.UnicodeText, default='')
additional_camera_connected = db.Column(db.Boolean, default=False)
firmware_version = db.Column(db.String, nullable=True, default=None)
archive_stream1 = db.Column(db.String, nullable=True, default=None)
archive_stream2 = db.Column(db.String, nullable=True, default=None)
confidence_stream = db.Column(db.String, nullable=True, default=None)
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)
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')
required_read_permissions = db.relationship('Permission', secondary=recorder_permission_table)
required_write_permissions = db.relationship('Permission', secondary=recorder_permission_table)
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_by_identifier(identifier):
return Recorder.query.filter(Recorder.id == identifier).first()
@staticmethod
def get_by_mac(mac: str):
if mac is None or mac == '':
return None
mac = mac.replace('-', ':').lower()
return Recorder.query.filter(Recorder._mac == mac).first()
@staticmethod
def get_all():
return Recorder.query.all()
@validates('name')
def validate_name(self, key, value):
assert len(value) > 2
return value
@hybrid_property
def configured_options(self) -> list:
return json.loads(self._configured_options_json_string)
@configured_options.setter
def configured_options(self, value: list):
self._configured_options_json_string = json.dumps(value)
def add_configured_option(self, value: str):
self._configured_options_json_string = json.dumps(self.configured_options.append(value))
@hybrid_property
def additional_notes(self) -> list:
return json.loads(self._additional_notes_json_string)
@additional_notes.setter
def additional_notes(self, value: list):
self._additional_notes_json_string = json.dumps(value)
def add_additional_notes(self, value: str):
self._additional_notes_json_string = json.dumps(self._additional_notes_json_string.append(value))
@hybrid_property
def mac(self) -> str:
return self._mac.upper()
@mac.setter
def mac(self, value: str):
if value is None or value == '':
return
if re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", value.lower()):
self._mac = value.replace('-', ':').lower()
else:
raise LrcException("'{}' is not a valid MAC Address!".format(value))
@hybrid_property
def ip(self) -> str:
return self._ip
@ip.setter
def ip(self, value: str):
try:
ipaddress.IPv4Interface(value)
self._ip = value
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
raise LrcException("'{}' is not a valid IPv4 Address!".format(value))
@hybrid_property
def ip6(self) -> str:
return self._ip6
@ip6.setter
def ip6(self, value: str):
try:
ipaddress.IPv6Interface(value)
self._ip6 = value
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
raise LrcException("'{}' is not a valid IPv6 Address!".format(value))
# handle bad ip
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 RecorderCommandPermission(db.Model, ModelBase):
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
recorder = db.relationship('Recorder')
recorder_id = db.Column(db.Integer, db.ForeignKey('recorder.id'))
recorder_command = db.relationship('RecorderCommand')
recorder_command_id = db.Column(db.Integer, db.ForeignKey('recorder_command.id'))
permission = db.relationship('Permission')
permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))
class RecorderCommand(db.Model, ModelBase):
__table_args__ = {'extend_existing': True}
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=datetime.utcnow())
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')
required_show_permissions = db.relationship('RecorderCommandPermission')
required_execute_permissions = db.relationship('RecorderCommandPermission')
@staticmethod
def get_all():
return RecorderCommand.query.all()
@staticmethod
def get_by_identifier(identifier):
return RecorderCommand.query.filter(RecorderCommand.id == identifier).first()
@property
def parameters(self) -> Union[dict, None]:
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)
if __name__ == '__main__':
Recorder(name="RecTest")

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""
Models for lecture recorder
"""
import json
import logging
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
logger = logging.getLogger("lrc."+__name__)
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_by_building_number(building_number):
"""
Find group by name
:param name:
:return:
"""
return Room.query.filter(Room.building_number == building_number)
@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()
logger.debug("tada")
logger.debug("got {} rooms".format(len(rooms)))
db_rooms = [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(db_rooms)
db.session.commit()
logger.debug("rooms commited to DB!")
except IntegrityError as e:
logger.error("Could not add rooms! ({})".format(e))
db.session.rollback()

View File

@@ -0,0 +1,543 @@
# -*- 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.config import Config
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'))
)
user_favorite_recorders_table = db.Table('user_favorite_recorders',
db.Column('user_id', db.Integer,
db.ForeignKey('user.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))
# 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))
# This is the association table for the many-to-many relationship between
# users and permissions.
user_permission_table = db.Table('user_permission',
db.Column('user_id', db.Integer,
db.ForeignKey('user.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')
permissions = db.relationship('Permission', secondary=user_permission_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')
favorite_recorders = db.relationship('Recorder', secondary=user_favorite_recorders_table)
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
@property
def effective_permissions(self):
permissions = Config.ROLE_PERMISSION_MAPPINGS.get(self.role, [])
for g in self.groups:
print(g)
for p in g.permissions:
print(p)
permissions.append(p)
return permissions
@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))
#read_only = db.Column(db.Boolean, default=False)
groups = db.relationship(Group, secondary=group_permission_table,
back_populates='permissions')
users = db.relationship(User, secondary=user_permission_table,
back_populates='permissions')
access_control_entry = db.relationship('AccessControlEntry', back_populates='required_permission')
@staticmethod
def get_by_name(name):
"""
Find permission by name
:param name:
:return:
"""
return Permission.query.filter(Permission.name == name).first()
@staticmethod
def get_all():
"""
Return all permissions
:return:
"""
return Permission.query.all()
@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,178 @@
import importlib
import inspect
import pkgutil
import sys
import telnetlib
from abc import ABC, abstractmethod
defined_recorder_adapters = None
# monkey patching of telnet lib
from pprint import pprint
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:
def __init__(self, address: str, user: str, password: str):
self.address = address
self.user = user
self.password = password
@classmethod
@abstractmethod
def get_recorder_params(cls) -> dict:
pass
@abstractmethod
def _get_name(self):
pass
@abstractmethod
def _get_version(self):
pass
@abstractmethod
def is_recording(self) -> bool:
pass
def get_recording_status(self) -> str:
pass
def get_defined_recorder_adapters() -> list:
rec_adapters_module = importlib.import_module(".recorder_adapters", package='backend')
rec_adapter_class = getattr(rec_adapters_module, "RecorderAdapter") # needed, otherwise subclass check may fail
models = []
found_packages = list(pkgutil.iter_modules(rec_adapters_module.__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': {}, 'path': rec_model_module.__file__}
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, rec_adapter_class) and name != "RecorderAdapter":
rec_model['id'] = rec_model['id'] + "." + obj.__name__
rec_model['class'] = obj
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
def get_recorder_adapter_by_id(id: str, **kwargs):
global defined_recorder_adapters
if defined_recorder_adapters is None:
defined_recorder_adapters = get_defined_recorder_adapters()
for rec_adapter in defined_recorder_adapters:
if id in rec_adapter.get('id', '').split("."):
return rec_adapter['class'](**kwargs)
return None
if __name__ == '__main__':
print(get_defined_recorder_adapters())
get_recorder_adapter_by_id('SMP35x', address="172.22.246.207", password="123mzsmp")
exit()

View File

@@ -0,0 +1,164 @@
# Copyright (c) 2019. Tobias Kurze
"""
This is the recorder adapter implementation for Epiphan Recorders. The following Epiphan recorder models are supported:
- LectureRecorder X2
- LectureRecorder
- VGADVI Recorder
- DVI Broadcaster DL
- DVIRecorderDL
"""
import shutil
import time
from datetime import datetime
from pprint import pprint
import requests
from requests.auth import HTTPBasicAuth
from requests.exceptions import ConnectionError
from backend import LrcException
from backend.recorder_adapters import RecorderAdapter
# HOST = "localhost"
from backend.tools.exception_decorator import exception_decorator
RECORDER_MODEL_NAME = "Epiphan Recorder Adapter (for: LectureRecorder X2, LectureRecorder, VGADVI Recorder, " \
"DVI Broadcaster DL and DVIRecorderDL)"
BASE_URL = "http://172.23.8.102" # Audimax SMP 351
USER = "admin"
PW = "lrgrashof+-"
class Epiphan(RecorderAdapter):
def __init__(self, address: str, user: str, password: str, firmware_version: str = "", **kwargs):
if not address.startswith('http'):
address = 'http://' + address
super().__init__(address, user, password)
self.firmware_version = firmware_version
self.session = requests.Session()
self.session.auth = HTTPBasicAuth(self.user, self.password)
@classmethod
def get_recorder_params(cls) -> dict:
return {'_requires_user': True,
'_requires_password': True}
def _get_name(self):
return RECORDER_MODEL_NAME
def _get_version(self):
pass
@exception_decorator(ConnectionError)
def get_recording_status(self) -> dict:
res = self.session.get(self.address + "/admin/ajax/recorder_status.cgi")
if res.ok:
return res.json()
raise LrcException(res.text, res.status_code)
@exception_decorator(ConnectionError)
def get_sysinfo(self) -> dict:
res = self.session.get(self.address + "/ajax/sysinfo.cgi")
if res.ok:
return res.json()
raise LrcException(res.text, res.status_code)
def is_recording(self) -> bool:
state = self.get_recording_status().get('state', None)
return state == "up"
def get_recording_time(self):
"""
Returns recording time in seconds. Also returns 0 if not recording.
:return:
"""
return self.get_recording_status().get('seconds', None)
def start_recording(self):
res = self.session.get(self.address + "/admin/ajax/start_recorder.cgi")
if not res.ok:
raise LrcException(res.text, res.status_code)
time.sleep(2) # just a little bit of waiting time -> it takes a bit for the Epiphan to update its state
def stop_recording(self):
res = self.session.get(self.address + "/admin/ajax/stop_recorder.cgi")
if not res.ok:
raise LrcException(res.text, res.status_code)
time.sleep(4) # just a little bit of waiting time -> it takes a bit for the Epiphan to update its state
def get_ip_address(self):
try:
return self.get_sysinfo().get("system").get("network").get("interfaces")[0].get('ipaddr', None)
except Exception as err:
raise LrcException(str(err))
def get_disk_space(self):
try:
data = self.get_sysinfo().get("system").get("data")
return {'available': data.get("available", None), 'free': data.get("free", None),
'total': data.get("total", None), 'used': data.get("total", 0) - data.get("available", 0)}
except Exception as err:
raise LrcException(str(err))
def get_video_inputs(self) -> list:
ret = []
try:
video = self.get_sysinfo().get("inputs").get("video")
for v in video:
ret.append(
{'id': v.get('id', None), 'name': v.get('name', None), 'resolution': v.get('resolution', None)})
return ret
except Exception as err:
raise LrcException(str(err))
def get_hardware_revision(self):
try:
return self.get_sysinfo().get("system").get("firmware")
except Exception as err:
raise LrcException(str(err))
def get_system_time(self):
try:
time_stamp = self.get_sysinfo().get("time")
return {'unix_time_stamp': time_stamp,
'date_time_utc': datetime.utcfromtimestamp(time_stamp).strftime('%Y-%m-%dT%H:%M:%SZ')}
except Exception as err:
raise LrcException(str(err))
def get_screenshot(self):
ret = self.session.get(self.address + "/admin/grab_frame.cgi?size=256x192&device=DAV93133.vga&_t=1573471990578",
stream=True)
print(ret)
pprint(ret.headers)
with open('out.jpg', 'wb') as out_file:
shutil.copyfileobj(ret.raw, out_file)
del ret
if __name__ == '__main__':
e = Epiphan(BASE_URL, USER, PW)
try:
# print(e.is_recording())
"""
if e.is_recording():
e.stop_recording()
else:
e.start_recording()
print(e.is_recording())
"""
"""
print(e.get_ip_address())
print(e.get_disk_space())
print(e.get_video_inputs())
print(e.get_hardware_revision())
print(e.get_system_time())
"""
e.get_screenshot()
except LrcException as e:
print(e)

View File

@@ -0,0 +1,846 @@
"""
Recorder Adapter for SMP
"""
import logging
from backend import LrcException
from backend.recorder_adapters import telnetlib, TelnetAdapter, RecorderAdapter
from backend.tools.exception_decorator import exception_decorator
logger = logging.getLogger("lrc.recorder_adapters.extron_smp")
RECORDER_MODEL_NAME = "Recorder Adapter for SMP 351 and 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
# HOST = "129.13.51.109" # Hertz
USER = "admin"
PW = "123mzsmp"
# PW = "audimaxsmp"
# PW = "smphertz"
class SMP35x(TelnetAdapter, RecorderAdapter):
def __init__(self, address, password, auto_login=True, **kwargs):
RecorderAdapter.__init__(self, address, "", password)
TelnetAdapter.__init__(self, address)
if auto_login:
self._login()
def _login(self):
logger.debug("Connecting to {} ...".format(self.address))
try:
self.tn = telnetlib.Telnet(self.address)
except TimeoutError as e:
raise LrcException(str(e))
except ConnectionRefusedError as e:
raise LrcException(str(e))
self.tn.read_until("\r\nPassword:")
# password = getpass.getpass()
password = self.password
self.tn.write(password + "\n\r")
out = self.tn.assert_string_in_output("Login Administrator")
# print(out)
if not out[0]:
# print(out[1])
if "Password:" in out[1]:
# TODO: loop until logged in...
logger.warning("Could not login (as admin) with given password! {}".format(self.address))
logger.debug("re-enter password")
self.tn.write(self.password + "\n\r")
# print(self.tn.assert_string_in_output("Login Administrator"))
self.tn = None
logger.error("Could definitely not login (as admin) with given password! {}".format(self.address))
raise LrcException("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 LrcException("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, mode: int = 2):
"""
Mode:
0 = IP config
2 = Box specific parameters
:param mode:
:return:
"""
if mode not in [0, 2]:
raise LrcException("Only values 0 and 2 are allowed!")
self.tn.write(self.esc_char + "1*{}XF\n".format(mode))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
def restore_configuration(self, mode: int = 2):
"""
Mode:
0 = IP config
2 = Box specific parameters
:param mode:
:return:
"""
if mode not in [0, 2]:
raise LrcException("Only values 0 and 2 are allowed!")
self.tn.write(self.esc_char + "0*{}XF\n".format(mode))
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
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 LrcException("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 LrcException("input_num must be a value between 1 and 5!")
if channel_num not in range(1, 3):
raise LrcException("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 LrcException("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 LrcException("input_num must be a value between 1 and 5!")
if input_format not in range(1, 4):
raise LrcException("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 LrcException("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 LrcException("input_num must be a value between 1 and 5!")
if len(input_name) > 16:
raise LrcException("input_name must be no longer than 16 chars")
try:
input_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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_selection_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())
@exception_decorator(ConnectionError)
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 int(TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line()))
def is_recording(self) -> bool:
return self.get_recording_status() == 1
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 LrcException("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 LrcException("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise LrcException("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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 LrcException("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 LrcException("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 129):
raise LrcException("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 LrcException("channel_number must be a value between 1 and 2!")
if preset_number not in range(1, 129):
raise LrcException("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 LrcException("preset_number must be a value between 1 and 128!")
if len(preset_name) > 16:
raise LrcException("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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 LrcException("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 LrcException("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise LrcException("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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 LrcException("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 LrcException("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("output_number must be a value between 1 and 3!")
if preset_number not in range(1, 17):
raise LrcException("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 LrcException("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise LrcException("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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 LrcException("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 LrcException("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 LrcException("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 LrcException("preset_number must be a value between 1 and 16!")
if len(preset_name) > 16:
raise LrcException("preset_name must be no longer than 16 chars")
try:
preset_name.encode('ascii')
except UnicodeEncodeError:
raise LrcException("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 LrcException("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 LrcException("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 LrcException("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 LrcException("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 LrcException("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
"""
@classmethod
def get_recorder_params(cls) -> dict:
return {'_requires_user': False,
'_requires_password': True}
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 LrcException("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 LrcException("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 LrcException("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 LrcException("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 = SMP35x(HOST, PW, True)
print(smp)
print(smp.get_recording_status())
print(smp.is_recording())
print(smp.get_input_presets())
print(smp.get_input_selection_per_channel())
print(smp.get_recording_status_text())
exit()
smp._login()
print(smp.get_version(verbose_info=False))
print(smp.get_file_transfer_config())
print(smp.save_configuration())
print(smp.restore_configuration())
return
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()

View File

@@ -0,0 +1,61 @@
import logging
from typing import Union
from backend.models import Recorder, RecorderCommand, LrcException
from backend.recorder_adapters import get_recorder_adapter_by_id
logger = logging.getLogger("lrc.recorder_adapters.helpers")
def validate_recorder_command_params(recorder_command: RecorderCommand, params: dict, fail_on_missing_params=False):
if recorder_command.parameters is None or len(recorder_command.parameters) == 0:
if len(params) == 0:
logger.debug("Number (0) of parameters matching expected number of args of command signature.")
return True
logger.info("More arguments specified ({}) than expected!".format(len(params)))
return False
for p_n in recorder_command.parameters:
p_t = recorder_command.parameters[p_n]
p = params.get(p_n, None)
if p is None:
if fail_on_missing_params:
return False
else:
if p_t == 'int':
params[p_n] = int(p)
return True
def get_function_from_recorder_command(recorder: Recorder, recorder_command: RecorderCommand,
connect_by_network_name=True):
adapter_name, function_name = recorder_command.name.split(':')
if connect_by_network_name and recorder.network_name is not None:
address = recorder.network_name
elif recorder.ip6 is not None:
address = recorder.ip6
else:
address = recorder.ip
logger.debug("Using {} to create recorder adapter to connect to {}.".format(address, recorder.name))
package_name, class_name = adapter_name.split('.')
adapter = get_recorder_adapter_by_id(class_name, address=address, user=recorder.username,
password=recorder.password, firmware_version=recorder.firmware_version)
logger.debug("Built adapter {}".format(adapter))
return getattr(adapter, function_name)
def execute_recorder_command(recorder: Recorder, recorder_command: RecorderCommand, params: Union[dict, None] = None,
connect_by_network_name=True):
if params is None:
params = dict()
if validate_recorder_command_params(recorder_command, params):
try:
func = get_function_from_recorder_command(recorder, recorder_command, connect_by_network_name)
logger.debug(
"Executing func: {} with params: '{}'".format(func, ", ".join(": ".join(str(_)) for _ in params.items())))
out = func(**params)
return True, out
except LrcException as e:
return False, str(e)
else:
logger.info("Could not validate given parameters!")
return False

77
backend/serve_frontend.py Normal file
View File

@@ -0,0 +1,77 @@
#!/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.")
print("FATAL: Frontend path wrong or index.html missing -> EXITING!")
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,13 @@
from backend import LrcException
def exception_decorator(*exceptions):
def decorator(func):
def new_func(*args, **kwargs):
try:
ret = func(*args, **kwargs)
return ret
except exceptions as e:
raise LrcException(e)
return new_func
return decorator

17
backend/tools/helpers.py Normal file
View File

@@ -0,0 +1,17 @@
import hashlib
def calculate_md5_checksum(string_to_md5_sum: str):
return hashlib.md5(string_to_md5_sum.encode('utf-8')).hexdigest()
def file_md5(fname: str) -> str:
hash_md5 = hashlib.md5()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
if __name__ == '__main__':
print(file_md5(__file__))

View File

@@ -0,0 +1,56 @@
import json
from pprint import pprint
import xlrd
MAX_CONSECUTIVE_EMPTY_ROWS_BEFORE_CANCEL = 5
file_location = "/home/tobias/tmp/Hoersaal-Rec-Liste.xlsx"
wb = xlrd.open_workbook(file_location)
sheet = wb.sheet_by_index(0)
headers = None
recorders = []
a_lot_of_empty_rows = False
consecutive_empty_rows = 0
ix = 0
while not a_lot_of_empty_rows:
try:
vals = sheet.row_values(ix)
except IndexError:
break
if len(set(vals)) == 1 and set(vals).pop() == '':
consecutive_empty_rows += 1
if consecutive_empty_rows > MAX_CONSECUTIVE_EMPTY_ROWS_BEFORE_CANCEL:
a_lot_of_empty_rows = True
else:
consecutive_empty_rows = 0
if len(set(vals)) > 5 and headers is None: # get table header
headers = {ix: vals[ix] for ix in range(0, len(vals))}
elif len(set(vals)) > 5: # regular rows
recorders.append({headers[ix]: vals[ix] for ix in range(0, len(vals))})
ix += 1
if ix >= 100:
a_lot_of_empty_rows = True
#pprint(recorders)
print(len(recorders))
nicely_formatted_recorders = []
for r in recorders:
rec = {'name': r['Opencast / CM'],
'building': r['Gebäude'],
'room': r['Hörsaal'],
'username': r['Benutzer'],
'password': r['PW'],
'ip': r['IP'],
'mac': r['MAC'],
'type': 'SMP 351' if 'SMP 351' in r['Rekorder-Typ'] else r['Rekorder-Typ'],
'additional_camera': r['Zus. Kamera'] == 'Ja',
'firmware_version': r['FW Vers.'],
'network_name': r['DNS'],
'description': json.dumps(
{'comment': r['Bemerkung'], 'network_port': r['Dosennummer'], 'rsync_name': r['Rsync-Name']})}
nicely_formatted_recorders.append(rec)
print(json.dumps(nicely_formatted_recorders))

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2019. Tobias Kurze
import hashlib
import json
import logging
import os
import re
from datetime import datetime
from json import dumps
from pprint import pprint
from sqlalchemy import and_
from sqlalchemy.exc import IntegrityError
from backend import db
from backend.models import Room
from backend.models.recorder_model import RecorderModel, RecorderCommand, Recorder
from backend.recorder_adapters import get_defined_recorder_adapters
from backend.tools.helpers import calculate_md5_checksum
logger = logging.getLogger("lrc." + __name__)
KNOWN_RECORDERS = {re.compile(r'(?P<name>SMP)[\s]*(?P<number>[\d]+)[\s]*.?[\s]*(?P<options>[\S]*)'): 'SMP',
re.compile(
r'(?P<name>LectureRecorder X2|LectureRecorder|VGADVI Recorder|DVI Broadcaster DL|DVIRecorderDL)'): 'Epiphan'}
def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel):
logger.debug("Received {} command definitions to create "
"for recoder model {}".format(len(command_definitions),
recorder_model.record_adapter_id))
existing_recorder_commands = RecorderCommand.query.filter(
and_(
RecorderCommand.name.in_([recorder_model.record_adapter_id + ":" + k for k 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):
logger.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:
args = command_definitions.get(c_d)
# print(args)
# create new recorder command(s)
r_c = RecorderCommand(name=recorder_model.record_adapter_id + ":" + c_d, parameters=args,
recorder_model=recorder_model)
db.session.add(r_c)
db.session.flush()
db.session.commit()
def update_recorder_models_database(drop: bool = False):
if drop:
for r in RecorderModel.get_all():
db.session.delete(r)
db.session.commit()
r_as = get_defined_recorder_adapters()
for r_a in r_as:
if r_a.get('class') is None: # skip modules without class (helpers.py, e.g.)
continue
logger.info("Checking Recorder Adapter {} for possible updates".format(r_a["id"]))
try:
r_m = RecorderModel.get_by_adapter_id(r_a["id"])
model_checksum = calculate_md5_checksum(dumps(r_a["commands"]))
logger.debug("Recorder Adapter commands checksum: {}".format(model_checksum))
logger.debug(r_m)
if r_m is None:
r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum,
**r_a.get('class').get_recorder_params())
db.session.add(r_m)
db.session.flush()
db.session.refresh(r_m)
logger.debug("Creating command definitions for rec mod adapter {}".format(r_m.record_adapter_id))
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
else:
logger.debug("Model command checksum already in db: {}".format(r_m.checksum))
if r_m.model_name != r_a["name"]:
r_m.model_name = r_a["name"]
r_m.last_time_modified = datetime.utcnow()
if model_checksum != r_m.checksum:
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
r_m.last_time_modified = datetime.utcnow()
r_m.checksum = model_checksum
r_m.last_checksum_change = datetime.utcnow()
logger.debug("Updating command definitions for rec mod adapter {}".format(r_m.record_adapter_id))
except IntegrityError as e:
logger.error(e)
db.session.rollback()
db.session.commit()
def get_recorder_room(rec: dict):
rooms = Room.get_by_building_number(rec.get('building', None))
if rooms.count() <= 0:
logger.warning("Building {} unknown! Can not find room for recorder {}.".format(rec['building'], rec['name']))
return None
if rooms.count() == 1:
return rooms.first()
room_name = rec.get('room')
room_name = room_name.replace('', ' ')
for room in rooms:
if all([r_n in room.name for r_n in room_name.split()]):
return room
logger.warning("No room found for recorder {}".format(rec['name']))
def create_default_recorders():
f = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'models', 'initial_recorders.json'))
with open(f, 'r') as json_file:
recorders = json.load(json_file)['recorders']
for r in recorders:
type = r.get('type')
firmware_version = r.get('firmware_version', None)
room_rec_name = r.get('name')
username = r.get('username', None)
password = r.get('password', None)
mac = r.get('mac', None)
ip4 = r.get('ip', None)
additional_camera = r.get('additional_camera', False)
description = r.get('description', '')
for k_r in KNOWN_RECORDERS:
if match := k_r.search(type):
name = match.groupdict().get('name')
model_number = match.groupdict().get('number', None)
options = match.groupdict().get('options', None)
if options is not None and options != "":
options = options.split(',')
if model_number is not None:
model_name = name + " " + model_number
model_search_name = name + model_number[:-1] # just get prefix (remove last digit); SMP 35x -> 35
else:
model_name = name
model_search_name = KNOWN_RECORDERS[k_r]
rec_model = RecorderModel.get_where_adapter_id_contains(model_search_name)
rec = Recorder.get_by_mac(mac)
if rec is None:
rec = Recorder.get_by_name(room_rec_name + " Recorder")
if rec is None:
rec = Recorder()
rec.mac = mac
db.session.add(rec)
rec.name = room_rec_name + " Recorder"
rec.model_name = model_name
rec.recorder_model = rec_model
rec.username = username
rec.password = password
rec.firmware_version = firmware_version
rec.ip = ip4
rec.additional_camera_connected = additional_camera
rec.additional_note = description
rec.configured_options = options
rec.room = get_recorder_room(r)
db.session.flush()
db.session.refresh(rec)
db.session.commit()
if __name__ == '__main__':
# for r in Room.get_all():
# print(r)
# exit()
commands = RecorderCommand.get_all()
for c in commands:
print(c)
print(c.recorder_model)
for r_m in RecorderModel.get_all():
print(r_m.recorder_commands)
exit()
recorders = Recorder.get_all()
for r in recorders:
if r.room is None:
print("{}: {}".format(r, r.room))
# db.drop_all()
# db.create_all()
update_recorder_models_database()
create_default_recorders()
db.session.commit()
# print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "10.50"}))
# print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "30.95"}))

View File

@@ -0,0 +1,300 @@
import json
import os
import logging
import subprocess
import threading
import time
from io import StringIO
from logging.handlers import MemoryHandler
from pprint import pprint
from typing import Union
import requests
from requests.auth import HTTPBasicAuth
from multiprocessing.pool import ThreadPool
from multiprocessing.context import TimeoutError
from ics import Calendar
from backend import LrcException
from backend.config import Config
from backend.models import Recorder, RecorderModel
from backend.recorder_adapters import RecorderAdapter
from backend.recorder_adapters.epiphan_base import Epiphan
from backend.recorder_adapters.extron_smp import SMP35x
from backend.tools.recorder_streams_sanity_checks import check_frame_is_valid, check_if_audio_is_valid
from backend.tools.send_mail import send_error_mail, get_smtp_error_handler
logger = logging.getLogger("lrc.tools.simple_state_checker")
smtp_error_handler = get_smtp_error_handler(subject="Errors have been detected while checking recorder states!")
# mem_handler = MemoryHandler(capacity=100, flushLevel=logging.FATAL, target=smtp_error_handler)
# mem_handler.setLevel(logging.WARNING)
rec_err_state_log_stream = StringIO()
rec_err_state_log_stream_handler = logging.StreamHandler(stream=rec_err_state_log_stream)
rec_err_state_log_stream_handler.setLevel(logging.WARNING)
logger.addHandler(rec_err_state_log_stream_handler)
# logger.addHandler(mem_handler)
session = requests.session()
session.auth = HTTPBasicAuth(Config.OPENCAST_USER, Config.OPENCAST_PW)
config = {'service_urls': {}}
recorders = None
agent_states_lock = threading.RLock()
agent_states = {}
def get_service_url(service_type: str):
if service_type in config['service_urls']:
return config['service_urls'][service_type]
params = {'serviceType': service_type}
url = Config.OPENCAST_URL + "/services/available.json"
res = session.get(url, params=params)
if res.ok:
service = res.json()["services"]["service"]
config["service_urls"][service_type] = service["host"] + \
service["path"]
return service["host"] + service["path"]
return None
def get_calender(rec_id):
params = {'agentid': rec_id}
url = get_service_url('org.opencastproject.scheduler') + "/calendars"
res = session.get(url, params=params)
if res.ok:
return Calendar(res.text)
def get_capture_agents():
url = get_service_url("org.opencastproject.capture.admin") + "/agents.json"
res = session.get(url)
if res.ok:
return res.json()["agents"]["agent"]
def get_recorder_details_old():
"""Temporary implementation using initial_recorders.json. Should be replaced by DB layer later!"""
global recorders
if recorders is None:
f = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'models', 'initial_recorders.json'))
with open(f, 'r') as json_file:
recorders = json.load(json_file)['recorders']
return recorders
def get_recorder_details():
"""New implementation using DB"""
global recorders
if recorders is None:
recorders = list(Recorder.get_all())
return recorders
def get_recorder_by_name(name: str):
for r in get_recorder_details():
logger.debug("Got recorder {}".format(r.get("name")))
if r.get("name") == name or r.get("name") + " Recorder" == name or r.get("name") == name + " Recorder":
return r
logger.error("Could not find recorder for name {}".format(name))
return None
def notify_users_of_problem(msg: str):
pass
def get_recorder_adapter(recorder_info: Union[dict, Recorder]) -> RecorderAdapter:
if recorder_info is None:
raise LrcException("Could not find recorder Adapter as recorder info was NONE!")
try:
type = recorder_info.get("type")
except KeyError:
type = RecorderModel.get_by_id(recorder_info.get('recorder_model_id')).model_name
if "SMP" in type:
rec = SMP35x(recorder_info.get('ip'), recorder_info.get('password'))
else:
rec = Epiphan(recorder_info.get('ip'), recorder_info.get("username"), recorder_info.get("password"))
return rec
def check_stream_sanity(recorder_agent: Union[Recorder, dict], recorder_adapter: RecorderAdapter = None):
if recorder_adapter is None:
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
recorder_adapter = get_recorder_adapter(recorder_info)
if not recorder_adapter.is_recording():
return True, "not recording, so there is no stream!", recorder_agent.get('name')
if recorder_agent.get('archive_stream1') is None and recorder_agent.get(
'archive_stream2') is None: # fall back to default names and rtsp
archive_stream_1_url = "rtsp://{}/{}".format(recorder_adapter.address, Config.DEFAULT_ARCHIVE_STREAM_1_NAME)
archive_stream_2_url = "rtsp://{}/{}".format(recorder_adapter.address, Config.DEFAULT_ARCHIVE_STREAM_2_NAME)
else:
archive_stream_1_url = recorder_agent.get('archive_stream1')
archive_stream_2_url = recorder_agent.get('archive_stream2')
frame_msg = frame_msg2 = sound_msg = sound_msg2 = "unknown"
for i in range(0, Config.STREAM_SANITY_CHECK_RETRIES):
frame_ok, frame_msg = check_frame_is_valid(archive_stream_1_url, raise_errors=False)
if not frame_ok:
logger.warning(
"Archive stream 1 ({}) of {} is not ok (frame): {}".format(archive_stream_1_url,
recorder_agent.get('name'),
frame_msg))
frame_ok, frame_msg2 = check_frame_is_valid(archive_stream_2_url, raise_errors=False)
if not frame_ok:
logger.warning(
"Archive stream 2 {} of ({}) is not ok: {} (frame)".format(archive_stream_2_url,
recorder_agent.get('name'),
frame_msg2))
sound_ok, sound_msg = check_if_audio_is_valid(archive_stream_1_url, raise_errors=False)
if not sound_ok:
logger.warning(
"Archive stream 1 {} of ({}) is not ok (audio): {}".format(archive_stream_1_url,
recorder_agent.get('name'),
sound_msg))
sound_ok, sound_msg2 = check_if_audio_is_valid(archive_stream_2_url, raise_errors=False)
if not sound_ok:
logger.warning(
"Archive stream 2 {} of ({}) is not ok (audio): {}".format(archive_stream_2_url,
recorder_agent.get('name'),
sound_msg2))
if frame_ok and sound_ok:
return True, "At least one archive stream is fine (audio and image)! " \
"(s1: a: {}, v: {}, s2: a: {}, v: {})".format(sound_msg, frame_msg, sound_msg2,
frame_msg2), recorder_agent.get('name')
else:
time.sleep(Config.STREAM_SANITY_CHECK_INTERVAL_SEC)
error_msg = "After {} retries, stream checks failed and returned: archive_stream1: audio:{}, frame:{}, " \
"archive_stream2: audio:{}, frame:{}".format(Config.STREAM_SANITY_CHECK_RETRIES, sound_msg,
frame_msg, sound_msg2, frame_msg2)
logger.error(error_msg)
return False, error_msg, recorder_agent.get('name') # stream sanity check failed!
def check_capture_agent_state(recorder_agent: Union[Recorder, dict]):
if recorder_agent.get('offline', False):
logger.info("OK - Recorder {} is in offline / maintenance mode".format(recorder_agent.get('name')))
return True, "Recorder is in offline / maintenance mode", recorder_agent.get('name')
agent_state_error_msg = None
logger.debug("Checking Agent {}".format(recorder_agent.get('name')))
c = get_calender(recorder_agent.get('name'))
is_recording_in_calendar = len(list(c.timeline.now())) >= 1
if is_recording_in_calendar:
logger.info("{} has entry in Calender and should therefore be recording... checking now!".format(
recorder_agent.get('name')))
if recorder_agent['state'] == "capturing":
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
try:
rec = get_recorder_adapter(recorder_info)
if rec.is_recording():
logger.info("OK recorder {} is recording :)".format(recorder_agent.get('name')))
with agent_states_lock:
agent_states[recorder_agent.get('name')] = 'OK - recorder is recording'
else:
logger.info(rec.get_recording_status())
logger.error(
"FATAL - recorder {} must be recording but is not!!!!".format(recorder_agent.get('name')))
agent_state_error_msg = "FATAL - recorder must be recording but is not!"
with agent_states_lock:
agent_states[recorder_agent['name']] = 'FATAL - recorder is NOT recording, but should!'
except LrcException as e:
logger.fatal("Exception occurred: {}".format(str(e)))
logger.error("Could not check state of recorder {}, Address: {}".format(recorder_agent.get('name'),
recorder_info.get('ip')))
else:
logger.error("FATAL: {} is not in capturing state...but should be!!".format(recorder_agent.get('name')))
agent_state_error_msg = "FATAL - is not in capturing state...but should be!"
else:
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
try:
rec = get_recorder_adapter(recorder_info)
if rec.is_recording():
logger.error("FATAL - recorder must not be recording!!!!")
agent_state_error_msg = "FATAL - is not in capturing state...but should be!"
with agent_states_lock:
agent_states[recorder_agent.get('name')] = 'FATAL - recorder IS recording, but should NOT!'
else:
logger.info("OK recorder is not recording :)")
with agent_states_lock:
agent_states[recorder_agent.get('name')] = 'OK - recorder is NOT recording'
except LrcException as e:
logger.fatal("Exception occurred: {}".format(str(e)))
logger.error("Could not check state of recorder {}, Address: {}".format(recorder_agent.get('name'),
recorder_info.get('ip')))
agent_state_error_msg = "FATAL - Could not check state of recorder! Address: {}".format(
recorder_info.get('ip'))
if agent_state_error_msg is None:
return True, agent_states[recorder_agent.get('name')], recorder_agent.get('name')
return False, agent_state_error_msg, recorder_agent.get('name')
def ping_capture_agent(recorder_agent: Union[Recorder, dict]):
if recorder_agent.get('offline', False):
print("is offline!")
logger.info("OK - Ping skipped, recorder {} is in offline mode.".format(recorder_agent.get('name')))
return True, "Recorder is in offline / maintenance mode", recorder_agent.get('name')
recorder_ip = get_recorder_by_name(recorder_agent.get('name')).get('ip')
try:
subprocess.check_call(
['ping', '-W', '10', '-c', '2', recorder_ip],
# stderr=subprocess.STDOUT, # get all output
stdout=subprocess.DEVNULL, # suppress output
stderr=subprocess.DEVNULL,
universal_newlines=True # return string not bytes
)
logger.info("Successfully pinged {} ({}). :-)".format(recorder_agent.get('name'), recorder_ip))
return True, "Successfully pinged {}. :-)".format(recorder_agent.get('name')), recorder_agent.get('name')
except subprocess.CalledProcessError:
logger.error("Can not ping {} ({})!!".format(recorder_agent.get('name'), recorder_ip))
return False, "Unable to ping {} ({})".format(recorder_agent.get('name'), recorder_ip), recorder_agent.get(
'name')
if __name__ == '__main__':
agents = get_capture_agents()
logger.info("Got {} capture agents that will be checked...".format(len(agents)))
for a in agents:
agent_states[a.get('name')] = 'PROBLEMATIC - unknown'
# pool = ThreadPool(5)
# pool.map(check_capture_agent_state, agents)
NUM_THREADS = 8
recorders = get_recorder_details()
with ThreadPool(NUM_THREADS) as pool:
# results = [pool.apply_async(ping_capture_agent, (agent,)) for agent in agents]
results = [pool.apply_async(ping_capture_agent, (agent,)) for agent in recorders]
try:
[res.get(timeout=12) for res in results]
except TimeoutError as e:
logger.error("Timeout while pinging capture agent! {}".format(e))
with ThreadPool(NUM_THREADS) as pool:
results = [pool.apply_async(check_capture_agent_state, (agent,)) for agent in agents]
try:
[res.get(timeout=12) for res in results]
except TimeoutError as e:
logger.error("Timeout while getting capture agent state! {}".format(e))
logger.info("DONE checking capture agents / recorders!")
logged_events = rec_err_state_log_stream.getvalue()
if len(logged_events) > 0:
logged_events += "\n\n=============\nAgent States:\n\n{}".format(''.join(
"{:<48}: {}\n".format(a, agent_states[a]) for a in agent_states
))
send_error_mail(logged_events, "Errors have been detected while checking recorder states!")
# mem_handler.close()

View File

@@ -0,0 +1,119 @@
import io
import logging
import sys
import ffmpeg
import os
import tempfile
from PIL import Image
from pydub import AudioSegment
from pydub.playback import play
logger = logging.getLogger("lrc.tools.recorder_streams_sanity_checks")
def is_single_color_image(image):
single_color_image = True
color = {}
count = 0
color_channels = image.split()
for c in color_channels: # r, g, b
hist = c.histogram()
num_of_non_zero_values = len([v for v in hist if v != 0])
logger.debug("color_channel: {}, num_of_non_zero_values (NON-BLACK): {}".format(c, num_of_non_zero_values))
if num_of_non_zero_values > 1:
single_color_image = False
break
else:
color[count] = [i for i in enumerate(hist) if i[1] != 0][0][0]
count += 1
return single_color_image, color
def check_frame_is_valid(stream_url, raise_errors=True):
try:
frame, _ = (
ffmpeg
.input(stream_url)
.output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
.run(capture_stdout=True, capture_stderr=True)
)
image = Image.open(io.BytesIO(frame))
single_color_image, color = is_single_color_image(image)
if not single_color_image:
image.show()
return True, "all good :-)"
else:
if all(map(lambda x: x == 0, color.values())):
return False, "Image is entirely black! (color: {} (RGB))".format(
':'.join([str(x) for x in color.values()]))
return False, "Image has only one single color! (color: {} (RGB))".format(
':'.join([str(x) for x in color.values()]))
except ffmpeg.Error as e:
msg = "Could not connect to stream URL or other error!"
logger.error(msg)
try:
msg += " ({})".format(e.stderr.decode('utf-8').strip().split("\n")[-1])
except IndexError:
pass
if raise_errors:
raise Exception(msg)
else:
return False, msg
def check_if_audio_is_valid(stream_url, sample_length_sec=3, lower_alert_limit_dBFS=-40.0, raise_errors=True):
file_name = tempfile.NamedTemporaryFile(suffix='.aac').name
if os.path.exists(file_name):
os.remove(file_name)
try:
frame, _ = (
ffmpeg
.input(stream_url, t=sample_length_sec)
.output(file_name)
.run(capture_stdout=True, capture_stderr=True)
)
sound = AudioSegment.from_file(file_name, "aac")
# print(sound.dBFS)
# play(sound)
if sound.max_dBFS == -float('inf'):
return False, "No active audio signal detected!"
elif sound.max_dBFS < lower_alert_limit_dBFS:
return False, "Very low volume (< {} dBFS) detected! ({})".format(lower_alert_limit_dBFS, sound.max_dBFS)
return True, "good audio signal detected! ({} max dBFS in {}s sample)".format(sound.max_dBFS, sample_length_sec)
except ffmpeg.Error as e:
msg = "Could not connect to stream URL or other error!"
logger.error(msg)
try:
msg += " ({})".format(e.stderr.decode('utf-8').strip().split("\n")[-1])
except IndexError:
pass
if raise_errors:
raise Exception(msg)
else:
return False, msg
"""
Following code is not working correctly - ffmpeg parameters are wrong.
"""
"""
def check_if_audio_is_valid_stream(stream_url, raise_errors=True):
audio, _ = (
ffmpeg
.input(stream_url, t=2)
.output('pipe:', f="adts", acodec='copy')
.run(capture_stdout=False, capture_stderr=False)
)
audio = io.BytesIO(audio)
sound = AudioSegment.from_file(audio, "aac")
play(sound)
# check_if_audio_is_valid('rtsp://172.22.246.207/extron1')
"""
if __name__ == '__main__':
# print(check_frame_is_valid('rtsp://172.22.246.207/extron2'))
print(check_if_audio_is_valid('rtsp://172.22.246.207/extron1'))

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__':
pprint(scrape_rooms())

View File

@@ -0,0 +1,60 @@
import smtplib
import traceback
from email.message import EmailMessage
from logging.handlers import SMTPHandler
from typing import List, Union
from backend import Config
def send_mail(subject: str, msg_body: str, to_mail: List, from_mail: str = Config.FROM_MAIL):
subject = subject.strip().replace("\r", "").split("\n")[0] # remove breaks resp. cut after line break
try:
msg = EmailMessage()
msg.set_content(msg_body)
msg["Subject"] = subject
msg["From"] = from_mail
msg["To"] = to_mail
s = smtplib.SMTP(Config.SMTP_SERVER)
s.send_message(msg)
s.quit()
except Exception as e:
print(
"Could not send E-Mail (Exception: {})".format(str(e)))
def send_error_mail(message_or_exception: Union[str, Exception], subject: str = None,
subject_prefix: str = "[LRC] Error: ",
receiver_mail_addresses=Config.ERROR_E_MAIL_TO):
if isinstance(message_or_exception, Exception):
if subject is None:
subject = subject_prefix + str(type(message_or_exception))
else:
subject = subject_prefix + subject
msg_body = str(type(message_or_exception)) + ": " + str(message_or_exception) + "\n\n" + traceback.format_exc()
else:
if subject is None:
subject = str(message_or_exception)
else:
subject = subject_prefix + subject
msg_body = str(message_or_exception)
send_mail(subject, msg_body, receiver_mail_addresses)
def send_warning_mail():
pass
def get_smtp_default_handler(receiver_mail_addresses=Config.ADMIN_E_MAIL_TO, subject: str = None,
subject_prefix: str = "[LRC] Log: "):
subject = subject if subject.startswith("[LRC]") else subject_prefix + subject
return SMTPHandler(Config.SMTP_SERVER, Config.FROM_MAIL, receiver_mail_addresses, subject)
def get_smtp_error_handler(receiver_mail_addresses=Config.ERROR_E_MAIL_TO, subject: str = None,
subject_prefix: str = "[LRC] Error: "):
subject = subject if subject.startswith("[LRC]") else subject_prefix + subject
return SMTPHandler(Config.SMTP_SERVER, Config.FROM_MAIL, receiver_mail_addresses, subject)

View File

@@ -0,0 +1,4 @@
# Copyright (c) 2019. Tobias Kurze
import backend.websocket.handlers

92
backend/websocket/base.py Normal file
View File

@@ -0,0 +1,92 @@
import logging
import threading
from flask_jwt_extended import verify_jwt_in_request, get_current_user, jwt_required, get_jwt_claims, get_jwt_identity
from flask_login import current_user
from flask_socketio import SocketIO, emit
from jwt import ExpiredSignatureError
from backend import app
logger = logging.getLogger("lrc.websocket")
async_mode = 'threading' # set to traditional python threading model
# async_mode = None
socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
class WebSocketBase:
def __init__(self, flask_app_context=None):
if flask_app_context is None:
self.flask_app_context = app
self.socket_thread = None
def start_websocket_in_thread(self, host=None, port=None, debug=None):
self.socket_thread = threading.Thread(
target=self.start_websocket,
args=(host, port, debug))
self.socket_thread.start()
return self.socket_thread
def start_websocket(self, host=None, port=None, debug=None):
if debug is None:
debug = self.flask_app_context.debug
socketio.run(self.flask_app_context, host=host, port=port, debug=debug)
def send_test_msg(self):
socketio.emit('test', "tolle nachricht")
@staticmethod
@socketio.on('connect')
def connect_handler():
logger.debug("new connection...")
try:
print(verify_jwt_in_request())
print(get_jwt_identity())
except ExpiredSignatureError:
logger.info("user is not authenticated! Signature expired.")
except Exception as e:
logger.info("user is not authenticated!")
print(str(e))
print(type(e))
print("not allowed!!")
return False # not allowed here
logger.debug("user is authenticated")
print("allowed!")
return True
@staticmethod
@socketio.on('message')
def handle_message(message):
print('received message: ' + message)
@staticmethod
@socketio.on('json')
def handle_json(json):
print('received json: ' + str(json))
@staticmethod
def _request_request_recorder_status_update():
pass
@staticmethod
def _test_on_msg_func():
pass
@staticmethod
@socketio.on('update_message_test_blabla')
def handle_msg(msg=None):
print('received msg: ' + str(msg))
socketio.on_event('my event', WebSocketBase._test_on_msg_func, namespace='/')
@staticmethod
@socketio.on_error()
def handle_error(error):
logger.error(error)
print(error)
if __name__ == '__main__':
wsb = WebSocketBase()
# wsb.start_websocket_in_thread(debug=True)
wsb.start_websocket(debug=True)

View File

@@ -0,0 +1,50 @@
import json
import logging
from backend.cron import async_cron_recorder_checker
from backend.websocket.base import socketio
logger = logging.getLogger("lrc.websocket.handlers")
recorder_state_checker = async_cron_recorder_checker
@socketio.on('request_recorder_state_updates')
def handle_request_recorder_state_updates_msg(recorder_id=None):
if recorder_id is None:
logger.warning("No recorder_id communicated, ignoring!")
return
logger.info("Adding recorder {} to state checker".format(recorder_id))
recorder_state_checker.add_object_to_state_check(recorder_id)
@socketio.on('force_recorder_state_update')
def handle_force_recorder_state_update_msg(recorder_id=None):
if recorder_id is None:
logger.warning("No recorder_id communicated, ignoring!")
return
current_states = recorder_state_checker.get_current_state()
for key in current_states:
state = current_states[key]
if state.get('id', None) == recorder_id:
logger.debug("Sending state to: {}".format(
'recorder_state_update_{}'.format(recorder_id)))
socketio.emit('recorder_state_update_{}'.format(recorder_id),
json.dumps(state))
return
logger.warning("Can't force update, no state found for recorder id {}.".format(recorder_id))
def send_state_update_to_recorders(recorder_results_dict: dict):
if len(recorder_results_dict) <= 0:
logger.debug("Sending state of recorders via web socket... => nothing to send!")
return
logger.debug("Sending state of recorders via web socket...")
for recorder_id in recorder_results_dict:
print(recorder_results_dict[recorder_id])
logger.debug("Sending state to: {}".format(
'recorder_state_update_{}'.format(recorder_results_dict[recorder_id].get('id'))))
socketio.emit('recorder_state_update_{}'.format(recorder_results_dict[recorder_id].get('id')),
json.dumps(recorder_results_dict[recorder_id]))

View File

@@ -0,0 +1,127 @@
# Copyright (c) 2019. Tobias Kurze
import logging
import threading
import time
from flask import Flask, request
from flask_socketio import SocketIO, emit
from backend import app
logging.basicConfig()
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
async_mode = 'threading'
async_mode = None
socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
thread = None
thread_lock = threading.Lock()
clients = []
def background_thread():
"""Example of how to send server generated events to clients."""
count = 0
while True:
socketio.sleep(3)
print("server_event ")
count += 1
socketio.emit('server_event',
{'data': 'Server generated event', 'count': count},
)
def ack():
print('message was received!')
@socketio.on('my_event')
def test_message(message: dict):
print("received msg!")
print(message)
if isinstance(message, str):
emit('my_response', 'tolle Antwort! (got only a string!)', callback=ack)
else:
emit('my_response', {'data': message['data']}, callback=ack)
@socketio.on('my broadcast event', namespace='/')
def test_message(message):
emit('my response', {'data': message['data']}, broadcast=True)
@socketio.on('connect')
def test_connect():
print("connected")
global thread
with thread_lock:
if thread is None:
thread = socketio.start_background_task(background_thread)
emit('connect', "You are connected!")
@socketio.on('disconnect', namespace='/')
def test_disconnect():
print('Client disconnected')
@socketio.on_error()
def handle_error(error):
print(error)
def start_in_thread():
socket_thread = threading.Thread(target=lambda appl, host, port: socketio.run(app=appl, host=host, port=port),
args=(app, "localhost", 5000))
socket_thread.start()
return socket_thread
@app.route("/")
def root():
return """<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8">
var socket = io();
socket.on('connect', function() {
console.log("connected!");
socket.emit('my_event', {data: 'I am connected!'});
});
socket.on('my_response', function(resp) {
console.log('got response:');
console.log(resp);
});
</script>"""
if __name__ == '__main__':
print("running main")
# socketio.init_app(app, cors_allowed_origins="*")
#while len(clients) <= 0:
# time.sleep(1)
#while True:
# time.sleep(2)
# print("sending message...")
# socketio.send("lala")
# print("send lala")
# print(clients[0])
# socketio.emit("server_event", {'msg': "tolle nachricht"}, room=clients[0])
# with app.test_request_context('/'):
# socketio.emit('server_event', "You!: server_event", namespace="/")
# print("send bla")
#socketio.run(app, host="localhost", port=5000, debug=True)
start_in_thread()
while True:
time.sleep(2)
print("still running! :)")
# socketio.run(app, debug=True)
# ENDE
# t.join()

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2019. Tobias Kurze
import logging
import threading
import time
from socketIO_client import SocketIO, LoggingNamespace
logging.basicConfig()
token = "# replace with: JWT Access Token"
#print(token)
print("params")
#socketIO = SocketIO('127.0.0.1', 5443, params={'jwt': '{}'.format(token)})
print("headers")
#socketIO = SocketIO('127.0.0.1', 5443, headers={'Authorization': 'Bearer {}'.format(token)})
print("cookies")
socketIO = SocketIO('127.0.0.1', 5443, cookies={'access_token_cookie': '{}'.format(token)})
#socketio = SocketIO(message_queue="redis://")
socketio = SocketIO('127.0.0.1', 5443)
#socketio.run(app, host="localhost", port=5000)
#socketio.init_app(app, host="localhost", port=5000, cors_allowed_origins="*", )
#socketio.init_app(app, host="localhost", port=5000, cors_allowed_origins="*", )
socketio.emit("server_event", {'data': 42, 'msg': 'toll'})
print("sent message!")
socketio.emit({'data': 42, 'msg': 'toll'})
print("sent message 2!")

View File

@@ -1,318 +0,0 @@
# -*- coding: utf-8 -*-
"""
Example user model and related models
"""
from backend import db, app
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_
from datetime import datetime, timedelta
from passlib.hash import sha256_crypt
from hashlib import md5
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'))
)
class User(UserMixin, db.Model):
"""
Example user model representation.
"""
id = db.Column(db.Integer, primary_key=True)
social_id = db.Column(db.String(64), nullable=True, unique=True)
nickname = db.Column(db.String(64), index=True, unique=True)
first_name = db.Column(db.String(64), index=True, nullable=True)
last_name = db.Column(db.String(64), index=True, nullable=True)
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
lang = db.Column(db.String(16), index=False, unique=False)
timezone = db.Column(db.String(32), index=False, unique=False)
posts = db.relationship('Post', backref='author', lazy='dynamic')
example_data_item = db.relationship('ExampleDataItem', backref='owner', lazy='dynamic')
about_me = db.Column(db.String(140))
role = db.Column(db.String(64))
password = db.Column(db.String(255), nullable=True)
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
last_seen = 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')
@staticmethod
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)).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)
@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
@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:
"""
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 __repr__(self):
return '<User %r>' % self.nickname
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 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

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from flask import render_template, send_from_directory, Blueprint
fe_path = os.path.join(os.getcwd(), "frontend", "dist")
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('/', defaults={'path': ''})
@fe_bp.route('/<path:path>')
def catch_all(path):
return render_template("index.html")