diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbb1f5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +*.db +*.pyc +app.pid + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-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 +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# 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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md index 1723adc..ffe821a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,14 @@ Dropper is an uploading/downloading application to push and pull data from devic # Notes * Need python 2+ * Set the fields in static/google-api-data.json file to use the google drive picker api. +* You will need Keycloak setup to use this application. +* DNS over HTTPS can affect hosts file usage so make sure to disable that if using Firefox and editing hosts file. -``` NOTE: These will get set from loadPicker... +# Setup +You will need Keycloak setup to use this application. The file 'client_secrets.json' has the predefined structure setup so you can use it for reference- modify accordingly. If you use the same realms and clients in Keycloak, you'll still need to change the 'client_secret' key; the one shown is an example of what you need to get from Keycloak (CHANGE and KEEP this SECRET if using on a public facing site!). In addition, use the hosts file on your computer to setup redirects for 'www.ssoapps.com' (Keycloak) and 'www.dropper.com' (Dropper App). + + +``` NOTE: These need to be set for loadPicker... (See: Dropper/dropper/static/google-api-data.json) // The Browser API key obtained from the Google API Console. // Replace with your own Browser API key, or your own key. let developerKey = ''; diff --git a/create_venv.sh b/create_venv.sh new file mode 100755 index 0000000..e39ae87 --- /dev/null +++ b/create_venv.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +. CONFIG.sh + +# set -o xtrace ## To debug scripts +# set -o errexit ## To exit on error +# set -o errunset ## To exit if a variable is referenced but not set + + +function main() { + rm -rf venv/ + + clear + python -m venv venv/ + sleep 2 + source "./venv/bin/activate" + + ANSR="-1" + while [[ $ANSR != "0" ]] && [[ $ANSR != "1" ]] && [[ $ANSR != "2" ]]; do + clear + menu_mesage + read -p "--> : " ANSR + done + case $ANSR in + "1" ) pip install -r linux-requirements.txt;; + "2" ) pip install -r windows-requirements.txt;; + "0" ) exit;; + * ) echo "Don't know how you got here but that's a bad sign...";; + esac +} + +function menu_mesage() { + echo "NOTE: Make sure to have Python 3 installed!" + echo -e "\nWhat do you want to do?" + echo -e "\t1) Generate Linux/Mac supported venv. (Installs Repuirements)" + echo -e "\t2) Generate Windows supported venv. (Installs Repuirements)" + echo -e "\t0) EXIT" +} + +main $@; diff --git a/requirements.txt b/linux-requirements.txt similarity index 88% rename from requirements.txt rename to linux-requirements.txt index 5dcb1f9..77dfd3c 100644 --- a/requirements.txt +++ b/linux-requirements.txt @@ -1,6 +1,7 @@ Click==7.0 Flask==1.1.1 Flask-Uploads==0.2.1 +flask-oidc==1.4.0 gunicorn==19.9.0 itsdangerous==1.1.0 Jinja2==2.10.3 diff --git a/src/dropper/__init__.py b/src/dropper/__init__.py index b080ef7..2496a96 100644 --- a/src/dropper/__init__.py +++ b/src/dropper/__init__.py @@ -1,7 +1,33 @@ +# system import +import os, secrets +from datetime import timedelta + +# Flask imports from flask import Flask, Blueprint +from flask_oidc import OpenIDConnect + + +ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__)) app = Flask(__name__) -app.secret_key = 'super secret key' -isDebugging = False +app.config.from_object("dropper.config.ProductionConfig") +# app.config.from_object("dropper.config.DevelopmentConfig") -from . import routes + +oidc = OpenIDConnect(app) +def oidc_loggedin(): + return oidc.user_loggedin + +def oidc_isAdmin(): + if oidc_loggedin(): + isAdmin = oidc.user_getfield("isAdmin") + if isAdmin == "yes" : + return True + return False + +app.jinja_env.globals['oidc_loggedin'] = oidc_loggedin +app.jinja_env.globals['oidc_isAdmin'] = oidc_isAdmin +app.jinja_env.globals['TITLE'] = app.config["TITLE"] + + +from dropper import routes diff --git a/src/dropper/client_secrets.json b/src/dropper/client_secrets.json new file mode 100644 index 0000000..ecfa274 --- /dev/null +++ b/src/dropper/client_secrets.json @@ -0,0 +1,14 @@ +{ + "web": { + "auth_uri": "https://www.ssoapps.com/auth/realms/apps/protocol/openid-connect/auth", + "client_id": "apps", + "issuer": "https://www.ssoapps.com/auth/realms/apps", + "client_secret": "9028c2ac-d6e0-4d96-86bd-02624b91695d", + "redirect_uris": [ + "https%3A%2F%2Fwww.dropper.com%2F" + ], + "userinfo_uri": "https://www.ssoapps.com/auth/realms/apps/protocol/openid-connect/userinfo", + "token_uri": "https://www.ssoapps.com/auth/realms/apps/protocol/openid-connect/token", + "token_introspection_uri": "https://www.ssoapps.com/auth/realms/apps/protocol/openid-connect/token/introspect" + } +} diff --git a/src/dropper/config.py b/src/dropper/config.py new file mode 100644 index 0000000..70544a0 --- /dev/null +++ b/src/dropper/config.py @@ -0,0 +1,54 @@ +# System import +import os, secrets +from datetime import timedelta + + +# Lib imports + + +# Apoplication imports + + +# Configs +APP_NAME = 'Dropper' + + +class Config(object): + TITLE = APP_NAME + DEBUG = False + TESTING = False + THREADED = True + SECRET_KEY = secrets.token_hex(32) + + HOME_PTH = os.path.expanduser("~") + ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__)) + + PERMANENT_SESSION_LIFETIME = timedelta(days = 7).total_seconds() + SQLALCHEMY_DATABASE_URI = "sqlite:///static/db/webfm.db" + SQLALCHEMY_TRACK_MODIFICATIONS = False + + OIDC_TOKEN_TYPE_HINT = 'access_token' + APP_REDIRECT_URI = "https%3A%2F%2Fwww.dropper.com%2F" # This path is submitted as the redirect URI in certain code flows + OIDC_CLIENT_SECRETS = ROOT_FILE_PTH + '/client_secrets.json' + OIDC_ID_TOKEN_COOKIE_SECURE = True + OIDC_REQUIRE_VERIFIED_EMAIL = False + OIDC_USER_INFO_ENABLED = True + OIDC_VALID_ISSUERS = [ + 'http://www.ssoapps.com/auth/realms/apps', + 'https://www.ssoapps.com/auth/realms/apps' + ] + + +class ProductionConfig(Config): + pass + + +class DevelopmentConfig(Config): + DEBUG = True + USE_RELOADER = True + OIDC_ID_TOKEN_COOKIE_SECURE = False + OIDC_REQUIRE_VERIFIED_EMAIL = False + + +class TestingConfig(Config): + TESTING = True diff --git a/src/dropper/routes.py b/src/dropper/routes/Routes.py similarity index 83% rename from src/dropper/routes.py rename to src/dropper/routes/Routes.py index 21d05d6..5044f1a 100644 --- a/src/dropper/routes.py +++ b/src/dropper/routes/Routes.py @@ -9,21 +9,21 @@ from flask_uploads import UploadSet, configure_uploads, ALL from werkzeug.utils import secure_filename # Application Imports -from . import app, isDebugging -from .MessageHandler import MessageHandler +from .. import app, oidc, utils, ROOT_FILE_PTH -msgHandler = MessageHandler() -SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/" -HOME_PTH = os.path.expanduser("~") - -NOTES_PTH = SCRIPT_PTH + 'static/' + "NOTES.txt" -UPLOADS_PTH = HOME_PTH + '/Downloads/' +msgHandler = utils.MessageHandler() +isDebugging = app.config["DEBUG"] +SCRIPT_PTH = app.config["ROOT_FILE_PTH"] +HOME_PTH = app.config["HOME_PTH"] +NOTES_PTH = SCRIPT_PTH + '/static/' + "NOTES.txt" +UPLOADS_PTH = HOME_PTH + '/Downloads/' files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH) list = [] # stores file name and hash -configure_uploads(app, files) + +configure_uploads(app, files) # Load notes... notesListEncoded = [] notesListDecoded = [] @@ -33,9 +33,9 @@ with open(NOTES_PTH) as infile: for entry in notesJson: notesListEncoded.append(entry) - decodedStrPart = base64.urlsafe_b64decode(entry.encode('utf-8')).decode('utf-8') + decodedStrPart = base64.urlsafe_b64decode(entry["string"].encode('utf-8')).decode('utf-8') entryDecoded = unquote(decodedStrPart) - notesListDecoded.append(entryDecoded) + notesListDecoded.append({"id": entry["id"], "string": entryDecoded }) except Exception as e: print(repr(e)) @@ -59,7 +59,8 @@ def root(): hash = hash_file(fpth) list.append([file, hash]) - return render_template('index.html', title="Dropper", files=list, notes=notesListDecoded) + return render_template('index.html', files = list, notes = notesListDecoded) + @app.route('/upload', methods=['GET', 'POST']) @@ -150,16 +151,15 @@ def deleteFile(): def deleteText(): if request.method == 'POST': try: - encodedStr = request.values['noteStr'].strip() - decodedStrPart = base64.urlsafe_b64decode(encodedStr.encode('utf-8')).decode('utf-8') - decodedStr = unquote(decodedStrPart) + noteIndex = int(request.values['noteIndex'].strip()) - if isDebugging: - print("Encoded String:\n\t" + encodedStr) - print("Decoded String:\n\t" + decodedStr) + i = 0 + for note in notesListDecoded: + if note["id"] == noteIndex: + notesListEncoded.pop(i) + notesListDecoded.pop(i) + i += 1 - notesListEncoded.remove(encodedStr) - notesListDecoded.remove(decodedStr) updateNotesFile() msg = "[Success] Deleted entry..." @@ -198,8 +198,9 @@ def addNote(): print("Encoded String:\n\t" + encodedStr) print("Decoded String:\n\t" + decodedStr) - notesListEncoded.append(encodedStr) - notesListDecoded.append(decodedStr) + strIndex = len(notesListEncoded) + 1 + notesListEncoded.append( {"id": strIndex, "string": encodedStr} ) + notesListDecoded.append( {"id": strIndex, "string": decodedStr} ) updateNotesFile() msg = "[Success] Added text entry!" @@ -222,7 +223,13 @@ def returnFile(id): def updateNotesFile(): with open(NOTES_PTH, 'w') as file: - file.write( json.dumps(notesListEncoded, indent=4) ) + i = 1 + objects = [] + for note in notesListEncoded: + objects.append({"id": i, "string": note["string"] }) + i += 1 + + file.write( json.dumps(objects, indent=4) ) file.close() diff --git a/src/dropper/routes/__init__.py b/src/dropper/routes/__init__.py new file mode 100644 index 0000000..ffe39af --- /dev/null +++ b/src/dropper/routes/__init__.py @@ -0,0 +1 @@ +from . import Routes diff --git a/src/dropper/static/css/main.css b/src/dropper/static/css/main.css index cb6591d..4969bc8 100644 --- a/src/dropper/static/css/main.css +++ b/src/dropper/static/css/main.css @@ -37,8 +37,8 @@ video { /* Classes */ .scroller { - scrollbar-color: #00000084 #ffffff64; - scrollbar-width: thin; + scrollbar-color: #00000084 #ffffff64; + scrollbar-width: thin; } .controls-secondary > button, @@ -69,8 +69,7 @@ video { } .server-image { - width: 16em; - height: 12em; + max-width: 24em; } /* Theme colors */ diff --git a/src/dropper/static/js/ajax.js b/src/dropper/static/js/ajax.js index d748fd8..f7728c5 100644 --- a/src/dropper/static/js/ajax.js +++ b/src/dropper/static/js/ajax.js @@ -36,13 +36,9 @@ const doAjaxUpload = (actionPath, data, fname, action) => { // For upload tracking with GET... xhttp.onprogress = function (e) { if (e.lengthComputable) { - percent = parseFloat( Math.floor( - ( - (e.loaded / e.total) * 100 ).toFixed(2) - ).toFixed(2) - ); - text = percent + '% Complete (' + fname + ')'; - if (percent <= 95) { + percent = (e.loaded / e.total) * 100; + text = parseFloat(percent).toFixed(2) + '% Complete (' + fname + ')'; + if (e.loaded !== e.total ) { updateProgressBar(progressbar, text, percent, "info"); } else { updateProgressBar(progressbar, text, percent, "success"); diff --git a/src/dropper/static/js/events.js b/src/dropper/static/js/events.js index 985cc14..d14c8ed 100644 --- a/src/dropper/static/js/events.js +++ b/src/dropper/static/js/events.js @@ -15,8 +15,8 @@ $( "#uploadText" ).bind( "click", function(eve) { }); $( "#deleteTextBtn" ).bind( "click", function(eve) { - const note = document.getElementById('toDeleteText').innerText; - deleteTextAction(eve.target, note); + const id = document.getElementById('deleteTextBtn').getAttribute("textstrid"); + deleteTextAction(eve.target, id); }); $( "#googlePicker" ).bind( "click", function(eve) { @@ -61,6 +61,7 @@ const preDeleteText = (elm = null) => { document.getElementById('toDeleteText').innerText = elm.innerText; document.getElementById('deleteTextBtn').setAttribute("textClass", elm.className); + document.getElementById('deleteTextBtn').setAttribute("textstrid", elm.getAttribute("textstrid")); } diff --git a/src/dropper/static/js/google-picker-logic.js b/src/dropper/static/js/google-picker-logic.js index bcfff75..dc21248 100644 --- a/src/dropper/static/js/google-picker-logic.js +++ b/src/dropper/static/js/google-picker-logic.js @@ -1,4 +1,5 @@ // NOTE: These will get set from loadPicker... +// Go to: https://console.developers.google.com/apis/credentials/oauthclient/ // The Browser API key obtained from the Google API Console. // Replace with your own Browser API key, or your own key. let developerKey = ''; diff --git a/src/dropper/static/js/post-ajax.js b/src/dropper/static/js/post-ajax.js index f480401..02adf1f 100644 --- a/src/dropper/static/js/post-ajax.js +++ b/src/dropper/static/js/post-ajax.js @@ -8,7 +8,12 @@ const postAjaxController = (data, action, hash, fname) => { message = data.message.text if (action === "upload-text" || action === "upload-file") { - displayMessage(message, type, "page-alert-zone-2", 3); + if (action === "upload-text") { + displayMessage(message, type, "page-alert-zone-text", 3); + } + if (action === "upload-file") { + displayMessage(message, type, "page-alert-zone-files", 3); + } return ; } diff --git a/src/dropper/static/js/ui-logic.js b/src/dropper/static/js/ui-logic.js index be2db69..3d8eeb2 100644 --- a/src/dropper/static/js/ui-logic.js +++ b/src/dropper/static/js/ui-logic.js @@ -22,7 +22,7 @@ const uploadFiles = (files = null) => { const size = files.length; if (files == null || size < 1) { - displayMessage("Nothing to upload...", "alert-warning", "page-alert-zone-2"); + displayMessage("Nothing to upload...", "warning", "page-alert-zone-2"); return ; } @@ -43,7 +43,7 @@ const uploadFiles = (files = null) => { const uploadTextEntry = (note = null) => { if (note == null || note == "") { - displayMessage("Nothing to upload...", "alert-warning", "page-alert-zone-2"); + displayMessage("Nothing to upload...", "warning", "page-alert-zone-2"); return ; } @@ -60,9 +60,9 @@ const deleteAction = async (hash) => { doAjax('delete-file', params, 'delete', hash); } -const deleteTextAction = (elm, note) => { - if (note == '' || note == undefined) { - displayMessage("No text to delete...", "alert-warning"); +const deleteTextAction = (elm, id) => { + if (id == '' || id == undefined) { + displayMessage("No text to delete...", "warning"); return; } @@ -70,9 +70,7 @@ const deleteTextAction = (elm, note) => { const refElm = document.getElementsByClassName(classRef)[0]; refElm.parentElement.removeChild(refElm); - // Encoded special characters then encode to b64 - encodedStr = window.btoa(encodeURIComponent(note)) - const params = new URLSearchParams('noteStr=' + encodedStr) + const params = new URLSearchParams('noteIndex=' + id) doAjax('delete-text', params, 'delete-text'); } diff --git a/src/dropper/templates/index.html b/src/dropper/templates/index.html index 46d4d3d..abc9357 100644 --- a/src/dropper/templates/index.html +++ b/src/dropper/templates/index.html @@ -9,15 +9,30 @@ -
{{note["string"]}}{% endif %}